Skip to content

Commit

Permalink
Refactor IntersectionType::describe()
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Oct 13, 2024
1 parent 96bd303 commit 67fbfae
Show file tree
Hide file tree
Showing 46 changed files with 601 additions and 199 deletions.
4 changes: 2 additions & 2 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -1314,7 +1314,7 @@ parameters:
-
message: '#^Doing instanceof PHPStan\\Type\\ArrayType is error\-prone and deprecated\. Use Type\:\:isArray\(\) or Type\:\:getArrays\(\) instead\.$#'
identifier: phpstanApi.instanceofType
count: 1
count: 3
path: src/Type/IntersectionType.php

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

-
Expand Down
152 changes: 111 additions & 41 deletions src/Type/IntersectionType.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace PHPStan\Type;

use PHPStan\Php\PhpVersion;
use PHPStan\PhpDocParser\Ast\Type\ArrayShapeNode;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IntersectionTypeNode;
Expand Down Expand Up @@ -45,7 +46,6 @@
use function ksort;
use function md5;
use function sprintf;
use function str_starts_with;
use function strcasecmp;
use function strlen;
use function substr;
Expand Down Expand Up @@ -347,6 +347,10 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes)

$nonEmptyStr = false;
$nonFalsyStr = false;
$isList = $this->isList()->yes();
$isArray = $this->isArray()->yes();
$isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
$describedTypes = [];
foreach ($this->getSortedTypes() as $i => $type) {
if ($type instanceof AccessoryNonEmptyStringType
|| $type instanceof AccessoryLiteralStringType
Expand Down Expand Up @@ -379,10 +383,45 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes)
$skipTypeNames[] = 'string';
continue;
}
if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
$typesToDescribe[$i] = $type;
$skipTypeNames[] = 'array';
continue;
if ($isList || $isArray) {
if ($type instanceof ArrayType) {
$keyType = $type->getKeyType();
$valueType = $type->getItemType();
if ($isList) {
$isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
$valueTypeDescription = '';
if (!$isMixedValueType) {
$valueTypeDescription = sprintf('<%s>', $valueType->describe($level));
}

$describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-list' : 'list') . $valueTypeDescription;
} else {
$isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
$isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
$typeDescription = '';
if (!$isMixedKeyType) {
$typeDescription = sprintf('<%s, %s>', $keyType->describe($level), $valueType->describe($level));
} elseif (!$isMixedValueType) {
$typeDescription = sprintf('<%s>', $valueType->describe($level));
}

$describedTypes[$i] = ($isNonEmptyArray ? 'non-empty-array' : 'array') . $typeDescription;
}
continue;
} elseif ($type instanceof ConstantArrayType) {
$description = $type->describe($level);
$descriptionWithoutKind = substr($description, strlen('array'));
$begin = $isList ? 'list' : 'array';
if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
$begin = 'non-empty-' . $begin;
}

$describedTypes[$i] = $begin . $descriptionWithoutKind;
continue;
}
if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
continue;
}
}

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

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

Expand All @@ -418,36 +456,6 @@ private function describeItself(VerbosityLevel $level, bool $skipAccessoryTypes)
}
}

if (
str_starts_with($typeDescription, 'array<')
&& in_array('array', $skipTypeNames, true)
) {
$nonEmpty = false;
$typeName = 'array';
foreach ($typesToDescribe as $j => $typeToDescribe) {
if (
$typeToDescribe instanceof AccessoryArrayListType
&& substr($typeDescription, 0, strlen('array<int<0, max>, ')) === 'array<int<0, max>, '
) {
$typeName = 'list';
$typeDescription = 'array<' . substr($typeDescription, strlen('array<int<0, max>, '));
} elseif ($typeToDescribe instanceof NonEmptyArrayType) {
$nonEmpty = true;
} else {
continue;
}

unset($typesToDescribe[$j]);
}

if ($nonEmpty) {
$typeName = 'non-empty-' . $typeName;
}

$describedTypes[$i] = $typeName . '<' . substr($typeDescription, strlen('array<'));
continue;
}

if (in_array($typeDescription, $skipTypeNames, true)) {
continue;
}
Expand Down Expand Up @@ -1139,6 +1147,10 @@ public function toPhpDocNode(): TypeNode

$nonEmptyStr = false;
$nonFalsyStr = false;
$isList = $this->isList()->yes();
$isArray = $this->isArray()->yes();
$isNonEmptyArray = $this->isIterableAtLeastOnce()->yes();
$describedTypes = [];

foreach ($this->getSortedTypes() as $i => $type) {
if ($type instanceof AccessoryNonEmptyStringType
Expand Down Expand Up @@ -1168,11 +1180,70 @@ public function toPhpDocNode(): TypeNode
$skipTypeNames[] = 'string';
continue;
}
if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
$typesToDescribe[$i] = $type;
$skipTypeNames[] = 'array';
continue;

if ($isList || $isArray) {
if ($type instanceof ArrayType) {
$keyType = $type->getKeyType();
$valueType = $type->getItemType();
if ($isList) {
$isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
$identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-list' : 'list');
if (!$isMixedValueType) {
$describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
$valueType->toPhpDocNode(),
]);
} else {
$describedTypes[$i] = $identifierTypeNode;
}
} else {
$isMixedKeyType = $keyType instanceof MixedType && $keyType->describe(VerbosityLevel::precise()) === 'mixed' && !$keyType->isExplicitMixed();
$isMixedValueType = $valueType instanceof MixedType && $valueType->describe(VerbosityLevel::precise()) === 'mixed' && !$valueType->isExplicitMixed();
$identifierTypeNode = new IdentifierTypeNode($isNonEmptyArray ? 'non-empty-array' : 'array');
if (!$isMixedKeyType) {
$describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
$keyType->toPhpDocNode(),
$valueType->toPhpDocNode(),
]);
} elseif (!$isMixedValueType) {
$describedTypes[$i] = new GenericTypeNode($identifierTypeNode, [
$valueType->toPhpDocNode(),
]);
} else {
$describedTypes[$i] = $identifierTypeNode;
}
}
continue;
} elseif ($type instanceof ConstantArrayType) {
$constantArrayTypeNode = $type->toPhpDocNode();
if ($constantArrayTypeNode instanceof ArrayShapeNode) {
$newKind = $constantArrayTypeNode->kind;
if ($isList) {
if ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
$newKind = ArrayShapeNode::KIND_NON_EMPTY_LIST;
} else {
$newKind = ArrayShapeNode::KIND_LIST;
}
} elseif ($isNonEmptyArray && !$type->isIterableAtLeastOnce()->yes()) {
$newKind = ArrayShapeNode::KIND_NON_EMPTY_ARRAY;
}

if ($newKind !== $constantArrayTypeNode->kind) {
if ($constantArrayTypeNode->sealed) {
$constantArrayTypeNode = ArrayShapeNode::createSealed($constantArrayTypeNode->items, $newKind);
} else {
$constantArrayTypeNode = ArrayShapeNode::createUnsealed($constantArrayTypeNode->items, $constantArrayTypeNode->unsealedType, $newKind);
}
}

$describedTypes[$i] = $constantArrayTypeNode;
continue;
}
}
if ($type instanceof NonEmptyArrayType || $type instanceof AccessoryArrayListType) {
continue;
}
}

if (!$type instanceof AccessoryType) {
$baseTypes[$i] = $type;
continue;
Expand All @@ -1186,7 +1257,6 @@ public function toPhpDocNode(): TypeNode
$typesToDescribe[$i] = $type;
}

$describedTypes = [];
foreach ($baseTypes as $i => $type) {
$typeNode = $type->toPhpDocNode();
if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'array') {
Expand Down
8 changes: 4 additions & 4 deletions tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4846,7 +4846,7 @@ public function dataArrayFunctions(): array
'array_pop($stringKeys)',
],
[
'array<stdClass>&hasOffsetValue(\'baz\', stdClass)',
'non-empty-array<stdClass>&hasOffsetValue(\'baz\', stdClass)',
'$stdClassesWithIsset',
],
[
Expand Down Expand Up @@ -8077,7 +8077,7 @@ public function dataArrayKeysInBranches(): array
'$array',
],
[
'array&hasOffsetValue(\'key\', mixed)',
'non-empty-array&hasOffsetValue(\'key\', mixed)',
'$generalArray',
],
[
Expand Down Expand Up @@ -8563,11 +8563,11 @@ public function dataIsset(): array
'$mixedIsset',
],
[
'array&hasOffset(\'a\')',
'non-empty-array&hasOffset(\'a\')',
'$mixedArrayKeyExists',
],
[
'array<int>&hasOffsetValue(\'a\', int)',
'non-empty-array<int>&hasOffsetValue(\'a\', int)',
'$integers',
],
[
Expand Down
4 changes: 2 additions & 2 deletions tests/PHPStan/Analyser/TypeSpecifierTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -1064,7 +1064,7 @@ public function dataCondition(): iterable
new Arg(new Variable('array')),
]),
[
'$array' => 'array&hasOffset(\'foo\')',
'$array' => 'non-empty-array&hasOffset(\'foo\')',
],
[
'$array' => '~hasOffset(\'foo\')',
Expand Down Expand Up @@ -1112,7 +1112,7 @@ public function dataCondition(): iterable
new Arg(new Variable('array')),
]),
[
'$array' => 'array&hasOffset(\'foo\')',
'$array' => 'non-empty-array&hasOffset(\'foo\')',
],
[
'$array' => '~hasOffset(\'foo\')',
Expand Down
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/data/param-out.php
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,7 @@ function foo16() {
function fooShuffle() {
$array = ["foo" => 123, "bar" => 456];
shuffle($array);
assertType('non-empty-array<0|1, 123|456>&list', $array);
assertType('non-empty-list<123|456>', $array);

$emptyArray = [];
shuffle($emptyArray);
Expand Down
8 changes: 4 additions & 4 deletions tests/PHPStan/Analyser/nsrt/array-chunk.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ public function chunkUnionTypeLength(array $arr, $positiveRange, $positiveUnion)
* @param int<50, max> $bigger50
*/
public function lengthIntRanges(array $arr, int $positiveInt, int $bigger50) {
assertType('list<non-empty-list<mixed>>', array_chunk($arr, $positiveInt));
assertType('list<non-empty-list<mixed>>', array_chunk($arr, $bigger50));
assertType('list<non-empty-list>', array_chunk($arr, $positiveInt));
assertType('list<non-empty-list>', array_chunk($arr, $bigger50));
}

/**
Expand All @@ -78,11 +78,11 @@ function testLimits(array $arr, int $oneToFour, int $tooBig) {
public function offsets(array $arr, array $map): void
{
if (array_key_exists('foo', $arr)) {
assertType('non-empty-list<non-empty-list<mixed>>', array_chunk($arr, 2));
assertType('non-empty-list<non-empty-list>', array_chunk($arr, 2));
assertType('non-empty-list<non-empty-array>', array_chunk($arr, 2, true));
}
if (array_key_exists('foo', $arr) && $arr['foo'] === 'bar') {
assertType('non-empty-list<non-empty-list<mixed>>', array_chunk($arr, 2));
assertType('non-empty-list<non-empty-list>', array_chunk($arr, 2));
assertType('non-empty-list<non-empty-array>', array_chunk($arr, 2, true));
}

Expand Down
6 changes: 3 additions & 3 deletions tests/PHPStan/Analyser/nsrt/array-column-php82.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public function testImprecise5(array $array): void
assertType('list<string>', array_column($array, 'nodeName'));
assertType('array<string, string>', array_column($array, 'nodeName', 'tagName'));
assertType('array<string, DOMElement>', array_column($array, null, 'tagName'));
assertType('list<mixed>', array_column($array, 'foo'));
assertType('list', array_column($array, 'foo'));
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
assertType('array<int|string, string>', array_column($array, 'nodeName', 'foo'));
assertType('array<int|string, DOMElement>', array_column($array, null, 'foo'));
Expand All @@ -187,7 +187,7 @@ public function testObjects1(array $array): void
assertType('non-empty-list<string>', array_column($array, 'nodeName'));
assertType('non-empty-array<string, string>', array_column($array, 'nodeName', 'tagName'));
assertType('non-empty-array<string, DOMElement>', array_column($array, null, 'tagName'));
assertType('list<mixed>', array_column($array, 'foo'));
assertType('list', array_column($array, 'foo'));
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
assertType('non-empty-array<int|string, string>', array_column($array, 'nodeName', 'foo'));
assertType('non-empty-array<int|string, DOMElement>', array_column($array, null, 'foo'));
Expand All @@ -199,7 +199,7 @@ public function testObjects2(array $array): void
assertType('array{string}', array_column($array, 'nodeName'));
assertType('non-empty-array<string, string>', array_column($array, 'nodeName', 'tagName'));
assertType('non-empty-array<string, DOMElement>', array_column($array, null, 'tagName'));
assertType('list<mixed>', array_column($array, 'foo'));
assertType('list', array_column($array, 'foo'));
assertType('array<string, mixed>', array_column($array, 'foo', 'tagName'));
assertType('non-empty-array<int|string, string>', array_column($array, 'nodeName', 'foo'));
assertType('non-empty-array<int|string, DOMElement>', array_column($array, null, 'foo'));
Expand Down
10 changes: 5 additions & 5 deletions tests/PHPStan/Analyser/nsrt/array-flip.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,25 +71,25 @@ function foo8($mixed)
function foo10(array $array)
{
if (array_key_exists('foo', $array)) {
assertType('array<string, int>&hasOffset(\'foo\')', $array);
assertType('non-empty-array<string, int>&hasOffset(\'foo\')', $array);
assertType('array<int, string>', array_flip($array));
}

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

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

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

0 comments on commit 67fbfae

Please sign in to comment.