Skip to content

Commit

Permalink
Use isArray, isConstantArray instead of instanceof in TypeCombinartor…
Browse files Browse the repository at this point in the history
…::union
  • Loading branch information
rajyan committed Dec 19, 2022
1 parent ab6c591 commit 679538e
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 114 deletions.
208 changes: 94 additions & 114 deletions src/Type/TypeCombinator.php
Expand Up @@ -20,9 +20,7 @@
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeFactory;
use PHPStan\Type\Generic\TemplateUnionType;
use function array_intersect_key;
use function array_key_exists;
use function array_keys;
use function array_map;
use function array_merge;
use function array_slice;
Expand Down Expand Up @@ -143,7 +141,6 @@ public static function union(Type ...$types): Type
}

$arrayTypes = [];
$arrayAccessoryTypes = [];
$scalarTypes = [];
$hasGenericScalarTypes = [];
for ($i = 0; $i < $typesCount; $i++) {
Expand All @@ -165,106 +162,26 @@ public static function union(Type ...$types): Type
if ($types[$i] instanceof StringType && !$types[$i] instanceof ClassStringType) {
$hasGenericScalarTypes[ConstantStringType::class] = true;
}
if ($types[$i] instanceof IntersectionType) {
$intermediateArrayType = null;
$intermediateAccessoryTypes = [];
foreach ($types[$i]->getTypes() as $innerType) {
if ($innerType instanceof TemplateType) {
continue 2;
}
if ($innerType instanceof ArrayType) {
$intermediateArrayType = $innerType;
continue;
}
if ($innerType instanceof AccessoryType || $innerType instanceof CallableType) {
if ($innerType instanceof HasOffsetValueType) {
$intermediateAccessoryTypes[sprintf('hasOffsetValue(%s)', $innerType->getOffsetType()->describe(VerbosityLevel::cache()))][] = $innerType;
continue;
}

$intermediateAccessoryTypes[$innerType->describe(VerbosityLevel::cache())][] = $innerType;
continue;
}
}

if ($intermediateArrayType !== null) {
$arrayTypes[] = $intermediateArrayType;
$arrayAccessoryTypes[] = $intermediateAccessoryTypes;
unset($types[$i]);
continue;
}
}
if (!$types[$i] instanceof ArrayType) {
if (!$types[$i]->isArray()->yes()) {
continue;
}

$arrayTypes[] = $types[$i];

$intermediateAccessoryTypes = [];
if ($types[$i]->isIterableAtLeastOnce()->yes()) {
$nonEmpty = new NonEmptyArrayType();
$intermediateAccessoryTypes[$nonEmpty->describe(VerbosityLevel::cache())] = [$nonEmpty];
}
if ($types[$i]->isList()->yes() && AccessoryArrayListType::isListTypeEnabled()) {
$list = new AccessoryArrayListType();
$intermediateAccessoryTypes[$list->describe(VerbosityLevel::cache())] = [$list];
}
$arrayAccessoryTypes[] = $intermediateAccessoryTypes;
unset($types[$i]);
}

foreach ($scalarTypes as $classType => $scalarTypeItems) {
$scalarTypes[$classType] = array_values($scalarTypeItems);
}

/** @var ArrayType[] $arrayTypes */
$arrayTypes = $arrayTypes;

$commonArrayAccessoryTypesKeys = [];
if (count($arrayAccessoryTypes) > 1) {
$commonArrayAccessoryTypesKeys = array_keys(array_intersect_key(...$arrayAccessoryTypes));
} elseif (count($arrayAccessoryTypes) > 0) {
$commonArrayAccessoryTypesKeys = array_keys($arrayAccessoryTypes[0]);
}

$arrayAccessoryTypesToProcess = [];
foreach ($commonArrayAccessoryTypesKeys as $commonKey) {
$typesToUnion = [];
foreach ($arrayAccessoryTypes as $array) {
foreach ($array[$commonKey] as $arrayAccessoryType) {
$typesToUnion[] = $arrayAccessoryType;
}
}
$arrayAccessoryTypesToProcess[] = self::union(...$typesToUnion);
}

$types = array_values(
array_merge(
$types,
self::processArrayTypes($arrayTypes, $arrayAccessoryTypesToProcess),
self::processArrayTypes($arrayTypes),
),
);
$typesCount = count($types);

// simplify string[] | int[] to (string|int)[]
for ($i = 0; $i < $typesCount; $i++) {
if (! $types[$i] instanceof IterableType) {
continue;
}

for ($j = $i + 1; $j < $typesCount; $j++) {
if ($types[$j] instanceof IterableType) {
$types[$i] = new IterableType(
self::union($types[$i]->getIterableKeyType(), $types[$j]->getIterableKeyType()),
self::union($types[$i]->getIterableValueType(), $types[$j]->getIterableValueType()),
);
array_splice($types, $j, 1);
$typesCount--;
continue 2;
}
}
}

foreach ($scalarTypes as $classType => $scalarTypeItems) {
if (isset($hasGenericScalarTypes[$classType])) {
unset($scalarTypes[$classType]);
Expand Down Expand Up @@ -405,6 +322,17 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array
return null;
}

// simplify string[] | int[] to (string|int)[]
if ($a instanceof IterableType && $b instanceof IterableType) {
return [
new IterableType(
self::union($a->getIterableKeyType(), $b->getIterableKeyType()),
self::union($a->getIterableValueType(), $b->getIterableValueType()),
),
null,
];
}

if ($a instanceof SubtractableType) {
$typeWithoutSubtractedTypeA = $a->getTypeWithoutSubtractedType();
if ($typeWithoutSubtractedTypeA instanceof MixedType && $b instanceof MixedType) {
Expand Down Expand Up @@ -572,33 +500,80 @@ private static function intersectWithSubtractedType(
}

/**
* @param ArrayType[] $arrayTypes
* @param Type[] $accessoryTypes
* @param Type[] $arrayTypes
* @return Type[]
*/
private static function processArrayTypes(array $arrayTypes, array $accessoryTypes): array
private static function processArrayAccessoryTypes(array $arrayTypes): array
{
foreach ($arrayTypes as $arrayType) {
if (!$arrayType instanceof ConstantArrayType) {
continue;
$accessoryTypes = [];
foreach ($arrayTypes as $i => $arrayType) {
if ($arrayType instanceof IntersectionType) {
foreach ($arrayType->getTypes() as $innerType) {
if ($innerType instanceof TemplateType) {
break;
}
if (!($innerType instanceof AccessoryType) && !($innerType instanceof CallableType)) {
continue;
}
if ($innerType instanceof HasOffsetValueType) {
$accessoryTypes[sprintf('hasOffsetValue(%s)', $innerType->getOffsetType()->describe(VerbosityLevel::cache()))][$i] = $innerType;
continue;
}

$accessoryTypes[$innerType->describe(VerbosityLevel::cache())][$i] = $innerType;
}
}
if (count($arrayType->getKeyTypes()) > 0) {

if (!$arrayType->isConstantArray()->yes()) {
continue;
}
$constantArrays = $arrayType->getConstantArrays();

foreach ($constantArrays as $constantArray) {
if ($constantArray->isList()->yes() && AccessoryArrayListType::isListTypeEnabled()) {
$list = new AccessoryArrayListType();
$accessoryTypes[$list->describe(VerbosityLevel::cache())][$i] = $list;
}

foreach ($accessoryTypes as $i => $accessoryType) {
if (!$accessoryType instanceof NonEmptyArrayType) {
if (!$constantArray->isIterableAtLeastOnce()->yes()) {
continue;
}

unset($accessoryTypes[$i]);
break 2;
$nonEmpty = new NonEmptyArrayType();
$accessoryTypes[$nonEmpty->describe(VerbosityLevel::cache())][$i] = $nonEmpty;
}
}

$commonAccessoryTypes = [];
$arrayTypeCount = count($arrayTypes);
foreach ($accessoryTypes as $accessoryType) {
if (count($accessoryType) !== $arrayTypeCount) {
continue;
}

if ($accessoryType[0] instanceof HasOffsetValueType) {
$commonAccessoryTypes[] = self::union(...$accessoryType);
continue;
}

$commonAccessoryTypes[] = $accessoryType[0];
}

return $commonAccessoryTypes;
}

/**
* @param Type[] $arrayTypes
* @return Type[]
*/
private static function processArrayTypes(array $arrayTypes): array
{
if ($arrayTypes === []) {
return [];
}

$accessoryTypes = self::processArrayAccessoryTypes($arrayTypes);

if (count($arrayTypes) === 1) {
return [
self::intersect(...$arrayTypes, ...$accessoryTypes),
Expand All @@ -614,27 +589,32 @@ private static function processArrayTypes(array $arrayTypes, array $accessoryTyp
$nextConstantKeyTypeIndex = 1;

foreach ($arrayTypes as $arrayType) {
if (!$arrayType instanceof ConstantArrayType || $generalArrayOccurred) {
$keyTypesForGeneralArray[] = $arrayType->getKeyType();
$valueTypesForGeneralArray[] = $arrayType->getItemType();
$generalArrayOccurred = true;
if ($generalArrayOccurred || !$arrayType->isConstantArray()->yes()) {
foreach ($arrayType->getArrays() as $type) {
$keyTypesForGeneralArray[] = $type->getKeyType();
$valueTypesForGeneralArray[] = $type->getItemType();
$generalArrayOccurred = true;
}
continue;
}

foreach ($arrayType->getKeyTypes() as $i => $keyType) {
$keyTypesForGeneralArray[] = $keyType;
$valueTypesForGeneralArray[] = $arrayType->getValueTypes()[$i];
$constantArrays = $arrayType->getConstantArrays();
foreach ($constantArrays as $constantArray) {
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
$keyTypesForGeneralArray[] = $keyType;
$valueTypesForGeneralArray[] = $constantArray->getValueTypes()[$i];

$keyTypeValue = $keyType->getValue();
if (array_key_exists($keyTypeValue, $constantKeyTypesNumbered)) {
continue;
}
$keyTypeValue = $keyType->getValue();
if (array_key_exists($keyTypeValue, $constantKeyTypesNumbered)) {
continue;
}

$constantKeyTypesNumbered[$keyTypeValue] = $nextConstantKeyTypeIndex;
$nextConstantKeyTypeIndex *= 2;
if (!is_int($nextConstantKeyTypeIndex)) {
$generalArrayOccurred = true;
continue;
$constantKeyTypesNumbered[$keyTypeValue] = $nextConstantKeyTypeIndex;
$nextConstantKeyTypeIndex *= 2;
if (!is_int($nextConstantKeyTypeIndex)) {
$generalArrayOccurred = true;
continue 2;
}
}
}
}
Expand Down Expand Up @@ -664,7 +644,7 @@ private static function reduceArrays(array $constantArrays): array
$arraysToProcess = [];
$emptyArray = null;
foreach ($constantArrays as $constantArray) {
if (!$constantArray instanceof ConstantArrayType) {
if (!$constantArray->isConstantArray()->yes()) {
$newArrays[] = $constantArray;
continue;
}
Expand All @@ -674,7 +654,7 @@ private static function reduceArrays(array $constantArrays): array
continue;
}

$arraysToProcess[] = $constantArray;
$arraysToProcess = array_merge($arraysToProcess, $constantArray->getConstantArrays());
}

if ($emptyArray !== null) {
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -1148,6 +1148,7 @@ public function dataFileAsserts(): iterable
}

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8520.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var-dynamic-return-type-extension-regression.php');
}

/**
Expand Down
@@ -0,0 +1,60 @@
<?php

namespace FilterVarDynamicReturnTypeExtensionRegression;

use PHPStan\Type\ConstantScalarType;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function PHPStan\Testing\assertType;

class Test {

/**
* @return array{default: Type, range?: Type}
*/
public function getOtherTypes()
{

}

public function determineExactType(): ?Type
{

}

public function test()
{

$exactType = $this->determineExactType();
$type = $exactType ?? new MixedType();
$otherTypes = $this->getOtherTypes();

assertType('array{default: PHPStan\Type\Type, range?: PHPStan\Type\Type}', $otherTypes);
if (isset($otherTypes['range'])) {
assertType('array{default: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes);
if ($type instanceof ConstantScalarType) {
if ($otherTypes['range']->isSuperTypeOf($type)->no()) {
$type = $otherTypes['default'];
}
assertType('array{default: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes);
unset($otherTypes['default']);
assertType('array{range: PHPStan\Type\Type}', $otherTypes);
} else {
$type = $otherTypes['range'];
assertType('array{default: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes);
}
assertType('array{default?: PHPStan\Type\Type, range: PHPStan\Type\Type}', $otherTypes);
}
assertType('array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}&non-empty-array', $otherTypes);
if ($exactType !== null) {
assertType('array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}&non-empty-array', $otherTypes);
unset($otherTypes['default']);
assertType('array{range?: PHPStan\Type\Type}', $otherTypes);
}
assertType('array{default?: PHPStan\Type\Type, range?: PHPStan\Type\Type}', $otherTypes);
if (isset($otherTypes['default']) && $otherTypes['default']->isSuperTypeOf($type)->no()) {
$type = TypeCombinator::union($type, $otherTypes['default']);
}
}
}

0 comments on commit 679538e

Please sign in to comment.