Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
parameters:
featureToggles:
bleedingEdge: true
preciseArrayShapeUnpacking: true
checkNonStringableDynamicAccess: true
checkParameterCastableToNumberFunctions: true
skipCheckGenericClasses!: []
Expand Down
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ parameters:
throwTypeCovariance: false
featureToggles:
bleedingEdge: false
preciseArrayShapeUnpacking: false
checkNonStringableDynamicAccess: false
checkParameterCastableToNumberFunctions: false
skipCheckGenericClasses:
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ parametersSchema:
])
featureToggles: structure([
bleedingEdge: bool(),
preciseArrayShapeUnpacking: bool(),
checkNonStringableDynamicAccess: bool(),
checkParameterCastableToNumberFunctions: bool(),
skipCheckGenericClasses: listOf(string()),
Expand Down
2 changes: 1 addition & 1 deletion src/DependencyInjection/ValidateIgnoredErrorsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public function resolveTypeAlias(string $aliasName, NameScope $nameScope): ?Type

}),
$constantResolver,
new InitializerExprTypeResolver($constantResolver, $reflectionProviderProvider, new PhpVersion(PHP_VERSION_ID), new OperatorTypeSpecifyingExtensionRegistry(new DirectExtensionsCollection([])), new UnaryOperatorTypeSpecifyingExtensionRegistry(new DirectExtensionsCollection([])), new OversizedArrayBuilder(), true),
new InitializerExprTypeResolver($constantResolver, $reflectionProviderProvider, new PhpVersion(PHP_VERSION_ID), new OperatorTypeSpecifyingExtensionRegistry(new DirectExtensionsCollection([])), new UnaryOperatorTypeSpecifyingExtensionRegistry(new DirectExtensionsCollection([])), new OversizedArrayBuilder(), true, $builder->parameters['featureToggles']['preciseArrayShapeUnpacking']),
reportUnsafeArrayStringKeyCasting: null,
),
),
Expand Down
100 changes: 97 additions & 3 deletions src/Reflection/InitializerExprTypeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@
use function dirname;
use function floor;
use function in_array;
use function intdiv;
use function intval;
use function is_finite;
use function is_float;
Expand All @@ -128,6 +129,7 @@ final class InitializerExprTypeResolver
{

public const CALCULATE_SCALARS_LIMIT = 128;
private const CALCULATE_ARRAYS_LIMIT = 32;

/** @var array<string, true> */
private array $currentlyResolvingClassConstant = [];
Expand All @@ -144,6 +146,8 @@ public function __construct(
private OversizedArrayBuilder $oversizedArrayBuilder,
#[AutowiredParameter]
private bool $usePathConstantsAsConstantString,
#[AutowiredParameter(ref: '%featureToggles.preciseArrayShapeUnpacking%')]
private bool $preciseArrayShapeUnpacking,
)
{
}
Expand Down Expand Up @@ -642,11 +646,99 @@ public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type
return $this->oversizedArrayBuilder->build($expr, $getTypeCallback);
}

$valueTypes = [];
$keyTypes = [];
if ($this->preciseArrayShapeUnpacking) {
$constantArrayVariantsByItemIndex = [];
$constantArraysCombinationsCount = 1;
$hasConstantArrayUnion = false;
$canResolveConstantArraysPrecisely = true;
foreach ($expr->items as $itemIndex => $arrayItem) {
$valueType = $getTypeCallback($arrayItem->value);
$valueTypes[$itemIndex] = $valueType;

if (!$arrayItem->unpack) {
$keyTypes[$itemIndex] = $arrayItem->key !== null
? $getTypeCallback($arrayItem->key)
: null;
continue;
}

$constantArrays = $valueType->getConstantArrays();
$constantArraysCount = count($constantArrays);
$constantArrayVariantsByItemIndex[$itemIndex] = $constantArrays;

if ($constantArraysCount === 0) {
$canResolveConstantArraysPrecisely = false;
continue;
}

if ($constantArraysCount > 1) {
$hasConstantArrayUnion = true;
}

if ($constantArraysCount > self::CALCULATE_ARRAYS_LIMIT
|| $constantArraysCombinationsCount > intdiv(self::CALCULATE_ARRAYS_LIMIT, $constantArraysCount)
) {
$canResolveConstantArraysPrecisely = false;
continue;
}

$constantArraysCombinationsCount *= $constantArraysCount;
}

if ($hasConstantArrayUnion && $canResolveConstantArraysPrecisely) {
$arrayBuilders = [ConstantArrayTypeBuilder::createEmpty()];
$keepStringKeys = $this->phpVersion->supportsArrayUnpackingWithStringKeys();

foreach ($expr->items as $itemIndex => $arrayItem) {
if (!$arrayItem->unpack) {
foreach ($arrayBuilders as $arrayBuilder) {
$arrayBuilder->setOffsetValueType(
$keyTypes[$itemIndex],
$valueTypes[$itemIndex],
);
}

continue;
}

$newArrayBuilders = [];

foreach ($arrayBuilders as $arrayBuilder) {
foreach ($constantArrayVariantsByItemIndex[$itemIndex] as $constantArray) {
$newArrayBuilder = clone $arrayBuilder;

foreach ($constantArray->getKeyTypes() as $j => $keyType) {
$newArrayBuilder->setOffsetValueType(
$keepStringKeys && $keyType->isString()->yes() ? $keyType : null,
$constantArray->getValueTypes()[$j],
$constantArray->isOptionalKey($j),
);
}

$newArrayBuilders[] = $newArrayBuilder;
}
}

$arrayBuilders = $newArrayBuilders;
}

$arrayTypes = [];

foreach ($arrayBuilders as $arrayBuilder) {
$arrayTypes[] = $arrayBuilder->getArray();
}

return TypeCombinator::union(...$arrayTypes);
}
}

$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();
$isList = null;
$hasOffsetValueTypes = [];
foreach ($expr->items as $arrayItem) {
$valueType = $getTypeCallback($arrayItem->value);
foreach ($expr->items as $itemIndex => $arrayItem) {
$valueType = $valueTypes[$itemIndex] ?? $getTypeCallback($arrayItem->value);
if ($arrayItem->unpack) {
$constantArrays = $valueType->getConstantArrays();
if (count($constantArrays) > 0) {
Expand Down Expand Up @@ -739,7 +831,9 @@ public function getArrayType(Expr\Array_ $expr, callable $getTypeCallback): Type
}
} else {
$arrayBuilder->setOffsetValueType(
$arrayItem->key !== null ? $getTypeCallback($arrayItem->key) : null,
array_key_exists($itemIndex, $keyTypes)
? $keyTypes[$itemIndex]
: ($arrayItem->key !== null ? $getTypeCallback($arrayItem->key) : null),
$valueType,
);
}
Expand Down
1 change: 1 addition & 0 deletions src/Testing/PHPStanTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ public static function createScopeFactory(ReflectionProvider $reflectionProvider
$container->getByType(UnaryOperatorTypeSpecifyingExtensionRegistry::class),
new OversizedArrayBuilder(),
$container->getParameter('usePathConstantsAsConstantString'),
$container->getParameter('featureToggles')['preciseArrayShapeUnpacking'],
);

return new ScopeFactory(
Expand Down
29 changes: 29 additions & 0 deletions tests/PHPStan/Analyser/PreciseArrayShapeUnpackingLegacyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Testing\TypeInferenceTestCase;
use PHPUnit\Framework\Attributes\DataProvider;

final class PreciseArrayShapeUnpackingLegacyTest extends TypeInferenceTestCase
{

public static function dataFileAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/precise-array-shape-unpacking-legacy.php');
}

/**
* @param mixed ...$args
*/
#[DataProvider('dataFileAsserts')]
public function testFileAsserts(
string $assertType,
string $file,
...$args,
): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}

}
36 changes: 36 additions & 0 deletions tests/PHPStan/Analyser/PreciseArrayShapeUnpackingTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Testing\TypeInferenceTestCase;
use PHPUnit\Framework\Attributes\DataProvider;

final class PreciseArrayShapeUnpackingTest extends TypeInferenceTestCase
{

public static function dataFileAsserts(): iterable
{
yield from self::gatherAssertTypes(__DIR__ . '/data/precise-array-shape-unpacking.php');
}

/**
* @param mixed ...$args
*/
#[DataProvider('dataFileAsserts')]
public function testFileAsserts(
string $assertType,
string $file,
...$args,
): void
{
$this->assertFileAsserts($assertType, $file, ...$args);
}

public static function getAdditionalConfigFiles(): array
{
return [
__DIR__ . '/../../../conf/bleedingEdge.neon',
];
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php // lint >= 8.1

declare(strict_types = 1);

namespace PreciseArrayShapeUnpackingLegacy;

use function PHPStan\Testing\assertType;

/**
* @param array{a: 1}|array{b: 2} $input
*/
function test(array $input): void
{
assertType('array{a?: 1, b?: 2}', [...$input]);
}
Loading
Loading