Skip to content

Commit

Permalink
Improve folding of constant arrays
Browse files Browse the repository at this point in the history
  • Loading branch information
rvanvelzen authored and ondrejmirtes committed Sep 22, 2022
1 parent 9077af6 commit 22eeeae
Show file tree
Hide file tree
Showing 9 changed files with 77 additions and 19 deletions.
29 changes: 27 additions & 2 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -1194,12 +1194,16 @@ public function isKeysSupersetOf(self $otherArray): bool

$failOnDifferentValueType = $keyTypesCount !== $otherKeyTypesCount || $keyTypesCount < 2;

$keyTypes = $this->keyTypes;

foreach ($otherArray->keyTypes as $j => $keyType) {
$i = $this->getKeyIndex($keyType);
$i = self::findKeyIndex($keyType, $keyTypes);
if ($i === null) {
return false;
}

unset($keyTypes[$i]);

$valueType = $this->valueTypes[$i];
$otherValueType = $otherArray->valueTypes[$j];
if (!$otherValueType->isSuperTypeOf($valueType)->no()) {
Expand All @@ -1212,6 +1216,18 @@ public function isKeysSupersetOf(self $otherArray): bool
$failOnDifferentValueType = true;
}

$requiredKeyCount = 0;
foreach (array_keys($keyTypes) as $i) {
if ($this->isOptionalKey($i)) {
continue;
}

$requiredKeyCount++;
if ($requiredKeyCount > 1) {
return false;
}
}

return true;
}

Expand Down Expand Up @@ -1246,7 +1262,16 @@ public function mergeWith(self $otherArray): self
*/
private function getKeyIndex($otherKeyType): ?int
{
foreach ($this->keyTypes as $i => $keyType) {
return self::findKeyIndex($otherKeyType, $this->keyTypes);
}

/**
* @param ConstantIntegerType|ConstantStringType $otherKeyType
* @param array<int, ConstantIntegerType|ConstantStringType> $keyTypes
*/
private static function findKeyIndex($otherKeyType, array $keyTypes): ?int
{
foreach ($keyTypes as $i => $keyType) {
if ($keyType->equals($otherKeyType)) {
return $i;
}
Expand Down
3 changes: 3 additions & 0 deletions src/Type/TypeCombinator.php
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,9 @@ private static function compareTypesInUnion(Type $a, Type $b): ?array
return [new HasOffsetValueType($a->getOffsetType(), self::union($a->getValueType(), $b->getValueType())), null];
}
}
if ($a instanceof ConstantArrayType && $b instanceof ConstantArrayType) {
return null;
}

if ($a instanceof SubtractableType) {
$typeWithoutSubtractedTypeA = $a->getTypeWithoutSubtractedType();
Expand Down
6 changes: 6 additions & 0 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,12 @@ public function testBug7140(): void
$this->assertNoErrors($errors);
}

public function testArrayUnion(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/array-union.php');
$this->assertNoErrors($errors);
}

/**
* @param string[]|null $allAnalysedFiles
* @return Error[]
Expand Down
14 changes: 7 additions & 7 deletions tests/PHPStan/Analyser/LegacyNodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2592,11 +2592,11 @@ public function dataBinaryOperations(): array
'$arrayOfIntegers += $arrayOfIntegers',
],
[
'array{0: 1, 1: 1, 2: 1, 3: 1|2, 4: 1|3, 5?: 2|3, 6?: 3}',
'array{1, 1, 1, 1, 1, 2, 3}|array{1, 1, 1, 1, 1}|array{1, 1, 1, 2, 3, 2, 3}|array{1, 1, 1, 2, 3}',
'$conditionalArray + $unshiftedConditionalArray',
],
[
'array{0: \'lorem\', 1: stdClass, 2: 1, 3: 1|2, 4: 1|3, 5?: 2|3, 6?: 3}',
'array{\'lorem\', stdClass, 1, 1, 1, 2, 3}|array{\'lorem\', stdClass, 1, 1, 1}',
'$unshiftedConditionalArray + $conditionalArray',
],
[
Expand Down Expand Up @@ -2672,7 +2672,7 @@ public function dataBinaryOperations(): array
'count($appendingToArrayInBranches)',
],
[
'int<3, 5>',
'3|5',
'count($conditionalArray)',
],
[
Expand Down Expand Up @@ -2936,7 +2936,7 @@ public function dataBinaryOperations(): array
'$arrToUnshift2',
],
[
'array{0: \'lorem\', 1: stdClass, 2: 1, 3: 1, 4: 1, 5?: 2|3, 6?: 3}',
'array{\'lorem\', stdClass, 1, 1, 1, 2, 3}|array{\'lorem\', stdClass, 1, 1, 1}',
'$unshiftedConditionalArray',
],
[
Expand Down Expand Up @@ -3008,7 +3008,7 @@ public function dataBinaryOperations(): array
'$anotherConditionalString . $conditionalString',
],
[
'int<6, 8>',
'6|8',
'count($conditionalArray) + count($array)',
],
[
Expand Down Expand Up @@ -4792,11 +4792,11 @@ public function dataArrayFunctions(): array
'$unknownArray',
],
[
'array{foo: \'banana\', bar: \'banana\', baz?: \'banana\', lorem?: \'banana\'}',
'array{foo: \'banana\', bar: \'banana\', baz: \'banana\', lorem: \'banana\'}|array{foo: \'banana\', bar: \'banana\'}',
'array_fill_keys($conditionalArray, \'banana\')',
],
[
'array{foo: stdClass, bar: stdClass, baz?: stdClass, lorem?: stdClass}',
'array{foo: stdClass, bar: stdClass, baz: stdClass, lorem: stdClass}|array{foo: stdClass, bar: stdClass}',
'array_map(function (): \stdClass {}, $conditionalKeysArray)',
],
[
Expand Down
24 changes: 24 additions & 0 deletions tests/PHPStan/Analyser/data/array-union.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace ArrayUnion;

use function PHPStan\Testing\assertType;

function doFoo(): bool
{
return (bool)random_int(0, 1);
}

function () {
$conditionalArray = [1, 1, 1];
assertType('array{1, 1, 1}', $conditionalArray);

if (doFoo()) {
$conditionalArray[] = 2;
$conditionalArray[] = 3;

assertType('array{1, 1, 1, 2, 3}', $conditionalArray);
}

assertType('array{1, 1, 1, 2, 3}|array{1, 1, 1}', $conditionalArray);
};
2 changes: 1 addition & 1 deletion tests/PHPStan/Analyser/data/bug-3558.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,6 @@ function (): void {
}

if(count($idGroups) > 1){
assertType('array{0: 1, 1?: array{1, 2}, 2?: array{1, 2}, 3?: array{1, 2}}', $idGroups);
assertType('array{1, array{1, 2}, array{1, 2}, array{1, 2}}|array{1}', $idGroups);
}
};
10 changes: 5 additions & 5 deletions tests/PHPStan/Analyser/data/constant-array-optional-set.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,17 +98,17 @@ public function doFoo()
$conditionalArray[] = 3;
}

assertType('array{0: 1, 1: 1, 2: 1, 3?: 2, 4?: 3}', $conditionalArray);
assertType('array{1, 1, 1, 2, 3}|array{1, 1, 1}', $conditionalArray);

$unshiftedConditionalArray = $conditionalArray;
array_unshift($unshiftedConditionalArray, 'lorem', new \stdClass());
assertType('array{0: \'lorem\', 1: stdClass, 2: 1, 3: 1, 4: 1, 5?: 2|3, 6?: 3}', $unshiftedConditionalArray);
assertType("array{'lorem', stdClass, 1, 1, 1, 2, 3}|array{'lorem', stdClass, 1, 1, 1}", $unshiftedConditionalArray);

assertType('array{0: 1, 1: 1, 2: 1, 3: 1|2, 4: 1|3, 5?: 2|3, 6?: 3}', $conditionalArray + $unshiftedConditionalArray);
assertType('array{0: \'lorem\', 1: stdClass, 2: 1, 3: 1|2, 4: 1|3, 5?: 2|3, 6?: 3}', $unshiftedConditionalArray + $conditionalArray);
assertType('array{1, 1, 1, 1, 1, 2, 3}|array{1, 1, 1, 1, 1}|array{1, 1, 1, 2, 3, 2, 3}|array{1, 1, 1, 2, 3}', $conditionalArray + $unshiftedConditionalArray);
assertType("array{'lorem', stdClass, 1, 1, 1, 2, 3}|array{'lorem', stdClass, 1, 1, 1}", $unshiftedConditionalArray + $conditionalArray);

$conditionalArray[] = 4;
assertType('array{0: 1, 1: 1, 2: 1, 3: 2|4, 4?: 3, 5?: 4}', $conditionalArray);
assertType('array{1, 1, 1, 2, 3, 4}|array{1, 1, 1, 4}', $conditionalArray);
}

}
2 changes: 1 addition & 1 deletion tests/PHPStan/Rules/Comparison/data/bug-7898.php
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ public function getCountryCode(): string
public function getHasDaycationTaxesAndFees(): bool
{
assertType("array{US: array{bar: array{sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, city_tax: array{type: 'both', unit: 'per-room-per-night'}, resort_fee: array{type: 'both', unit: 'per-room-per-night'}, additional_tax_or_fee: array{type: 'both', unit: 'per-room-per-night'}}, foo: array{tax: array{type: 'rate', unit: 'per-room-per-night'}}}, CA: array{bar: array{goods_and_services_tax: array{type: 'rate', unit: 'per-room-per-night'}, provincial_sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, harmonized_sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, municipal_and_regional_district_tax: array{type: 'rate', unit: 'per-room-per-night'}, additional_tax_or_fee: array{type: 'both', unit: 'per-room-per-night'}}}, SG: array{bar: array{service_charge: array{type: 'rate', unit: 'per-room-per-night'}, tax: array{type: 'rate', unit: 'per-room-per-night'}}}, TH: array{bar: array{service_charge: array{type: 'rate', unit: 'per-room-per-night'}, tax: array{type: 'rate', unit: 'per-room-per-night'}, city_tax: array{type: 'rate', unit: 'per-room-per-night'}}}, AE: array{bar: array{vat: array{type: 'rate', unit: 'per-room-per-night'}, service_charge: array{type: 'rate', unit: 'per-room-per-night'}, municipality_fee: array{type: 'rate', unit: 'per-room-per-night'}, tourism_fee: array{type: 'both', unit: 'per-room-per-night'}, destination_fee: array{type: 'both', unit: 'per-room-per-night'}}}, BH: array{bar: array{vat: array{type: 'rate', unit: 'per-room-per-night'}, service_charge: array{type: 'rate', unit: 'per-room-per-night'}, city_tax: array{type: 'rate', unit: 'per-room-per-night'}}}, HK: array{bar: array{service_charge: array{type: 'rate', unit: 'per-room-per-night'}, tax: array{type: 'rate', unit: 'per-room-per-night'}}}, ES: array{bar: array{city_tax: array{type: 'both', unit: 'per-room-per-night'}}}}", FooEnum::APPLICABLE_TAX_AND_FEES_BY_TYPE);
assertType("array{bar: array{goods_and_services_tax: array{type: 'rate', unit: 'per-room-per-night'}, provincial_sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, harmonized_sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, municipal_and_regional_district_tax: array{type: 'rate', unit: 'per-room-per-night'}, additional_tax_or_fee: array{type: 'both', unit: 'per-room-per-night'}}}|array{bar: array{sales_tax?: array{type: 'rate', unit: 'per-room-per-night'}, city_tax: array{type: 'both', unit: 'per-room-per-night'}, resort_fee?: array{type: 'both', unit: 'per-room-per-night'}, additional_tax_or_fee?: array{type: 'both', unit: 'per-room-per-night'}}, foo?: array{tax: array{type: 'rate', unit: 'per-room-per-night'}}}|array{bar: array{service_charge: array{type: 'rate', unit: 'per-room-per-night'}, tax: array{type: 'rate', unit: 'per-room-per-night'}, city_tax?: array{type: 'rate', unit: 'per-room-per-night'}}}|array{bar: array{vat: array{type: 'rate', unit: 'per-room-per-night'}, service_charge: array{type: 'rate', unit: 'per-room-per-night'}, city_tax: array{type: 'rate', unit: 'per-room-per-night'}}}|array{bar: array{vat: array{type: 'rate', unit: 'per-room-per-night'}, service_charge: array{type: 'rate', unit: 'per-room-per-night'}, municipality_fee: array{type: 'rate', unit: 'per-room-per-night'}, tourism_fee: array{type: 'both', unit: 'per-room-per-night'}, destination_fee: array{type: 'both', unit: 'per-room-per-night'}}}", FooEnum::APPLICABLE_TAX_AND_FEES_BY_TYPE[$this->getCountryCode()]);
assertType("array{bar: array{city_tax: array{type: 'both', unit: 'per-room-per-night'}}|array{sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, city_tax: array{type: 'both', unit: 'per-room-per-night'}, resort_fee: array{type: 'both', unit: 'per-room-per-night'}, additional_tax_or_fee: array{type: 'both', unit: 'per-room-per-night'}}, foo?: array{tax: array{type: 'rate', unit: 'per-room-per-night'}}}|array{bar: array{goods_and_services_tax: array{type: 'rate', unit: 'per-room-per-night'}, provincial_sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, harmonized_sales_tax: array{type: 'rate', unit: 'per-room-per-night'}, municipal_and_regional_district_tax: array{type: 'rate', unit: 'per-room-per-night'}, additional_tax_or_fee: array{type: 'both', unit: 'per-room-per-night'}}}|array{bar: array{service_charge: array{type: 'rate', unit: 'per-room-per-night'}, tax: array{type: 'rate', unit: 'per-room-per-night'}, city_tax?: array{type: 'rate', unit: 'per-room-per-night'}}}|array{bar: array{vat: array{type: 'rate', unit: 'per-room-per-night'}, service_charge: array{type: 'rate', unit: 'per-room-per-night'}, city_tax: array{type: 'rate', unit: 'per-room-per-night'}}}|array{bar: array{vat: array{type: 'rate', unit: 'per-room-per-night'}, service_charge: array{type: 'rate', unit: 'per-room-per-night'}, municipality_fee: array{type: 'rate', unit: 'per-room-per-night'}, tourism_fee: array{type: 'both', unit: 'per-room-per-night'}, destination_fee: array{type: 'both', unit: 'per-room-per-night'}}}", FooEnum::APPLICABLE_TAX_AND_FEES_BY_TYPE[$this->getCountryCode()]);
return array_key_exists(FooEnum::FOO_TYPE, FooEnum::APPLICABLE_TAX_AND_FEES_BY_TYPE[$this->getCountryCode()]);
}

Expand Down
6 changes: 3 additions & 3 deletions tests/PHPStan/Rules/Variables/EmptyRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,15 @@ public function testRule(): void
$this->strictUnnecessaryNullsafePropertyFetch = false;
$this->analyse([__DIR__ . '/data/empty-rule.php'], [
[
'Offset \'nonexistent\' on array{0?: bool, 1?: false, 2: bool, 3: false, 4: true} in empty() does not exist.',
'Offset \'nonexistent\' on array{2: bool, 3: false, 4: true}|array{bool, false, bool, false, true} in empty() does not exist.',
22,
],
[
'Offset 3 on array{0?: bool, 1?: false, 2: bool, 3: false, 4: true} in empty() always exists and is always falsy.',
'Offset 3 on array{2: bool, 3: false, 4: true}|array{bool, false, bool, false, true} in empty() always exists and is always falsy.',
24,
],
[
'Offset 4 on array{0?: bool, 1?: false, 2: bool, 3: false, 4: true} in empty() always exists and is not falsy.',
'Offset 4 on array{2: bool, 3: false, 4: true}|array{bool, false, bool, false, true} in empty() always exists and is not falsy.',
25,
],
[
Expand Down

0 comments on commit 22eeeae

Please sign in to comment.