Skip to content

Commit

Permalink
TypeCombinator - fix unions of intersections
Browse files Browse the repository at this point in the history
  • Loading branch information
ondrejmirtes committed Aug 9, 2018
1 parent da47536 commit e346d3f
Show file tree
Hide file tree
Showing 4 changed files with 115 additions and 17 deletions.
3 changes: 3 additions & 0 deletions src/Type/Accessory/HasOffsetType.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public function accepts(Type $type, bool $strictTypes): TrinaryLogic

public function isSuperTypeOf(Type $type): TrinaryLogic
{
if ($this->equals($type)) {
return TrinaryLogic::createYes();
}
return $type->isOffsetAccessible()
->and($type->hasOffsetValueType($this->offsetType));
}
Expand Down
57 changes: 43 additions & 14 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace PHPStan\Type;

use PHPStan\Type\Accessory\AccessoryType;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantBooleanType;
Expand Down Expand Up @@ -93,6 +94,7 @@ public static function union(Type ...$types): Type

$typesCount = count($types);
$arrayTypes = [];
$arrayAccessoryTypes = [];
$scalarTypes = [];
$hasGenericScalarTypes = [];
for ($i = 0; $i < $typesCount; $i++) {
Expand Down Expand Up @@ -121,6 +123,27 @@ public static function union(Type ...$types): Type
if ($types[$i] instanceof StringType) {
$hasGenericScalarTypes[ConstantStringType::class] = true;
}
if ($types[$i] instanceof IntersectionType) {
$intermediateArrayType = null;
$intermediateAccessoryTypes = [];
foreach ($types[$i]->getTypes() as $innerType) {
if ($innerType instanceof ArrayType) {
$intermediateArrayType = $innerType;
continue;
}
if ($innerType instanceof AccessoryType) {
$intermediateAccessoryTypes[] = $innerType;
continue;
}
}

if ($intermediateArrayType !== null) {
$arrayTypes[] = $intermediateArrayType;
$arrayAccessoryTypes = array_merge($arrayAccessoryTypes, $intermediateAccessoryTypes);
unset($types[$i]);
continue;
}
}
if (!$types[$i] instanceof ArrayType) {
continue;
}
Expand All @@ -133,7 +156,7 @@ public static function union(Type ...$types): Type
$arrayTypes = $arrayTypes;

$types = array_values(
array_merge($types, self::processArrayTypes($arrayTypes))
array_merge($types, self::processArrayTypes($arrayTypes, $arrayAccessoryTypes))
);

// simplify string[] | int[] to (string|int)[]
Expand Down Expand Up @@ -201,12 +224,17 @@ public static function union(Type ...$types): Type

/**
* @param ArrayType[] $arrayTypes
* @return ArrayType[]
* @param AccessoryType[] $accessoryTypes
* @return Type[]
*/
private static function processArrayTypes(array $arrayTypes): array
private static function processArrayTypes(array $arrayTypes, array $accessoryTypes): array
{
if (count($arrayTypes) < 2) {
return $arrayTypes;
if (count($arrayTypes) === 0) {
return [];
} elseif (count($arrayTypes) === 1) {
return [
self::intersect($arrayTypes[0], ...$accessoryTypes),
];
}

$keyTypesForGeneralArray = [];
Expand Down Expand Up @@ -243,12 +271,16 @@ private static function processArrayTypes(array $arrayTypes): array
}
}

$createGeneralArray = function () use ($keyTypesForGeneralArray, $valueTypesForGeneralArray, $accessoryTypes): Type {
return TypeCombinator::intersect(new ArrayType(
self::union(...$keyTypesForGeneralArray),
self::union(...$valueTypesForGeneralArray)
), ...$accessoryTypes);
};

if ($generalArrayOcurred) {
return [
new ArrayType(
self::union(...$keyTypesForGeneralArray),
self::union(...$valueTypesForGeneralArray)
),
$createGeneralArray(),
];
}

Expand Down Expand Up @@ -290,10 +322,7 @@ private static function processArrayTypes(array $arrayTypes): array

if (count($constantArraysBuckets) > self::CONSTANT_ARRAY_UNION_THRESHOLD) {
return [
new ArrayType(
self::union(...$keyTypesForGeneralArray),
self::union(...$valueTypesForGeneralArray)
),
$createGeneralArray(),
];
}

Expand All @@ -304,7 +333,7 @@ private static function processArrayTypes(array $arrayTypes): array
$builder->setOffsetValueType($data['keyType'], $data['valueType']);
}

$resultArrays[] = $builder->getArray();
$resultArrays[] = self::intersect($builder->getArray(), ...$accessoryTypes);
}

return $resultArrays;
Expand Down
6 changes: 3 additions & 3 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5920,17 +5920,17 @@ public function dataForeachLoopVariables(): array
"'afterLoop'",
],
[
'array<string, 1|2|3>',
'array<string, 1|2|3>&hasOffset(string)',
'$this->property',
"'begin'",
],
[
'array<string, 1|2|3>',
'array<string, 1|2|3>&hasOffset(string)',
'$this->property',
"'end'",
],
[
'array<string, 1|2|3>',
'array<string, 1|2|3>&hasOffset(string)',
'$this->property',
"'afterLoop'",
],
Expand Down
66 changes: 66 additions & 0 deletions tests/PHPStan/Type/TypeCombinatorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,34 @@ public function dataUnion(): array
StringType::class,
'string',
],
[
[
new IntersectionType([
new ArrayType(new MixedType(), new StringType()),
new HasOffsetType(new StringType()),
]),
new IntersectionType([
new ArrayType(new MixedType(), new StringType()),
new HasOffsetType(new StringType()),
]),
],
IntersectionType::class,
'array<string>&hasOffset(string)',
],
[
[
new IntersectionType([
new ObjectWithoutClassType(),
new HasPropertyType('foo'),
]),
new IntersectionType([
new ObjectWithoutClassType(),
new HasPropertyType('foo'),
]),
],
IntersectionType::class,
'object&hasProperty(foo)',
],
];
}

Expand Down Expand Up @@ -1175,6 +1203,33 @@ public function dataIntersect(): array
IntersectionType::class,
'array<string, string>&hasOffset(\'a\')',
],
[
[
new ArrayType(new StringType(), new StringType()),
new HasOffsetType(new ConstantStringType('a')),
new HasOffsetType(new ConstantStringType('a')),
],
IntersectionType::class,
'array<string, string>&hasOffset(\'a\')',
],
[
[
new ArrayType(new StringType(), new StringType()),
new HasOffsetType(new StringType()),
new HasOffsetType(new StringType()),
],
IntersectionType::class,
'array<string, string>&hasOffset(string)',
],
[
[
new ArrayType(new MixedType(), new MixedType()),
new HasOffsetType(new StringType()),
new HasOffsetType(new StringType()),
],
IntersectionType::class,
'array&hasOffset(string)',
],
[
[
new ConstantArrayType(
Expand Down Expand Up @@ -1260,6 +1315,17 @@ public function dataIntersect(): array
ClosureType::class,
'Closure<mixed>',
],
[
[
new UnionType([
new ArrayType(new MixedType(), new StringType()),
new NullType(),
]),
new HasOffsetType(new StringType()),
],
UnionType::class,
'(array<string>&hasOffset(string))|null',
],
];
}

Expand Down

0 comments on commit e346d3f

Please sign in to comment.