Skip to content

Commit

Permalink
Improve ConstantArrayType::unsetOffset
Browse files Browse the repository at this point in the history
  • Loading branch information
herndlm committed Jul 21, 2022
1 parent 27a25d6 commit 7af6056
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 24 deletions.
61 changes: 40 additions & 21 deletions src/Type/Constant/ConstantArrayType.php
Original file line number Diff line number Diff line change
Expand Up @@ -575,31 +575,50 @@ public function setOffsetValueType(?Type $offsetType, Type $valueType, bool $uni
public function unsetOffset(Type $offsetType): Type
{
$offsetType = ArrayType::castToArrayKeyType($offsetType);
if ($offsetType instanceof ConstantIntegerType || $offsetType instanceof ConstantStringType) {

$results = [];
foreach (TypeUtils::flattenTypes($offsetType) as $flattenType) {
if (!$flattenType instanceof ConstantIntegerType && !$flattenType instanceof ConstantStringType) {
return new ArrayType($this->getKeyType(), $this->getItemType());
}

$hasKey = false;
foreach ($this->keyTypes as $i => $keyType) {
if ($keyType->getValue() === $offsetType->getValue()) {
$keyTypes = $this->keyTypes;
unset($keyTypes[$i]);
$valueTypes = $this->valueTypes;
unset($valueTypes[$i]);

$newKeyTypes = [];
$newValueTypes = [];
$newOptionalKeys = [];

$k = 0;
foreach ($keyTypes as $j => $newKeyType) {
$newKeyTypes[] = $newKeyType;
$newValueTypes[] = $valueTypes[$j];
if (in_array($j, $this->optionalKeys, true)) {
$newOptionalKeys[] = $k;
}
$k++;
}
if ($keyType->getValue() !== $flattenType->getValue()) {
continue;
}

return new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys);
$hasKey = true;
$keyTypes = $this->keyTypes;
unset($keyTypes[$i]);
$valueTypes = $this->valueTypes;
unset($valueTypes[$i]);

$newKeyTypes = [];
$newValueTypes = [];
$newOptionalKeys = [];

$k = 0;
foreach ($keyTypes as $j => $newKeyType) {
$newKeyTypes[] = $newKeyType;
$newValueTypes[] = $valueTypes[$j];
if (in_array($j, $this->optionalKeys, true)) {
$newOptionalKeys[] = $k;
}
$k++;
}

$results[] = new self($newKeyTypes, $newValueTypes, $this->nextAutoIndexes, $newOptionalKeys);
break;
}
if ($hasKey) {
continue;
}

$results[] = $this;
}
if ($results !== []) {
return TypeCombinator::union(...$results);
}

return new ArrayType($this->getKeyType(), $this->getItemType());
Expand Down
4 changes: 1 addition & 3 deletions tests/PHPStan/Analyser/AnalyserIntegrationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,7 @@ public function testBug7215(): void
public function testBug7094(): void
{
$errors = $this->runAnalyse(__DIR__ . '/data/bug-7094.php');
$this->assertCount(7, $errors);
$this->assertCount(6, $errors);

$this->assertSame('Parameter #2 $val of method Bug7094\Foo::setAttribute() contains unresolvable type.', $errors[0]->getMessage());
$this->assertSame(74, $errors[0]->getLine());
Expand All @@ -808,8 +808,6 @@ public function testBug7094(): void

$this->assertSame('Parameter #1 $attr of method Bug7094\Foo::setAttributes() expects array{foo?: string, bar?: 5|6|7, baz?: bool}, non-empty-array<string, 5|6|7|bool|string> given.', $errors[5]->getMessage());
$this->assertSame(29, $errors[5]->getLine());
$this->assertSame('Parameter #1 $attr of method Bug7094\Foo::setAttributes() expects array{foo?: string, bar?: 5|6|7, baz?: bool}, array<\'bar\'|\'baz\'|\'foo\', 5|6|7|bool|string> given.', $errors[6]->getMessage());
$this->assertSame(49, $errors[6]->getLine());
}

public function testOffsetAccess(): void
Expand Down
3 changes: 3 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -935,6 +935,9 @@ public function dataFileAsserts(): iterable
yield from $this->gatherAssertTypes(__DIR__ . '/data/this-subtractable.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/match-expression-inference.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-1519.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7621-1.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7621-2.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-7621-3.php');
}

/**
Expand Down
148 changes: 148 additions & 0 deletions tests/PHPStan/Analyser/data/bug-7621-1.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<?php declare(strict_types = 1);

namespace Bug7621;

use function PHPStan\Testing\assertType;

class Foo
{

private const GROUP_PUBLIC_CONSTANTS = 'public constants';
private const GROUP_PROTECTED_CONSTANTS = 'protected constants';
private const GROUP_PRIVATE_CONSTANTS = 'private constants';
private const GROUP_PUBLIC_PROPERTIES = 'public properties';
private const GROUP_PUBLIC_STATIC_PROPERTIES = 'public static properties';
private const GROUP_PROTECTED_PROPERTIES = 'protected properties';
private const GROUP_PROTECTED_STATIC_PROPERTIES = 'protected static properties';
private const GROUP_PRIVATE_PROPERTIES = 'private properties';
private const GROUP_PRIVATE_STATIC_PROPERTIES = 'private static properties';
private const GROUP_CONSTRUCTOR = 'constructor';
private const GROUP_STATIC_CONSTRUCTORS = 'static constructors';
private const GROUP_DESTRUCTOR = 'destructor';
private const GROUP_MAGIC_METHODS = 'magic methods';
private const GROUP_PUBLIC_METHODS = 'public methods';
private const GROUP_PUBLIC_ABSTRACT_METHODS = 'public abstract methods';
private const GROUP_PUBLIC_FINAL_METHODS = 'public final methods';
private const GROUP_PUBLIC_STATIC_METHODS = 'public static methods';
private const GROUP_PUBLIC_STATIC_ABSTRACT_METHODS = 'public static abstract methods';
private const GROUP_PUBLIC_STATIC_FINAL_METHODS = 'public static final methods';
private const GROUP_PROTECTED_METHODS = 'protected methods';
private const GROUP_PROTECTED_ABSTRACT_METHODS = 'protected abstract methods';
private const GROUP_PROTECTED_FINAL_METHODS = 'protected final methods';
private const GROUP_PROTECTED_STATIC_METHODS = 'protected static methods';
private const GROUP_PROTECTED_STATIC_ABSTRACT_METHODS = 'protected static abstract methods';
private const GROUP_PROTECTED_STATIC_FINAL_METHODS = 'protected static final methods';
private const GROUP_PRIVATE_METHODS = 'private methods';
private const GROUP_PRIVATE_STATIC_METHODS = 'private static methods';

private const GROUP_SHORTCUT_CONSTANTS = 'constants';
private const GROUP_SHORTCUT_PROPERTIES = 'properties';
private const GROUP_SHORTCUT_STATIC_PROPERTIES = 'static properties';
private const GROUP_SHORTCUT_METHODS = 'methods';
private const GROUP_SHORTCUT_PUBLIC_METHODS = 'all public methods';
private const GROUP_SHORTCUT_PROTECTED_METHODS = 'all protected methods';
private const GROUP_SHORTCUT_PRIVATE_METHODS = 'all private methods';
private const GROUP_SHORTCUT_STATIC_METHODS = 'static methods';
private const GROUP_SHORTCUT_ABSTRACT_METHODS = 'abstract methods';
private const GROUP_SHORTCUT_FINAL_METHODS = 'final methods';

private const SHORTCUTS = [
self::GROUP_SHORTCUT_CONSTANTS => [
self::GROUP_PUBLIC_CONSTANTS,
self::GROUP_PROTECTED_CONSTANTS,
self::GROUP_PRIVATE_CONSTANTS,
],
self::GROUP_SHORTCUT_STATIC_PROPERTIES => [
self::GROUP_PUBLIC_STATIC_PROPERTIES,
self::GROUP_PROTECTED_STATIC_PROPERTIES,
self::GROUP_PRIVATE_STATIC_PROPERTIES,
],
self::GROUP_SHORTCUT_PROPERTIES => [
self::GROUP_SHORTCUT_STATIC_PROPERTIES,
self::GROUP_PUBLIC_PROPERTIES,
self::GROUP_PROTECTED_PROPERTIES,
self::GROUP_PRIVATE_PROPERTIES,
],
self::GROUP_SHORTCUT_PUBLIC_METHODS => [
self::GROUP_PUBLIC_FINAL_METHODS,
self::GROUP_PUBLIC_STATIC_FINAL_METHODS,
self::GROUP_PUBLIC_ABSTRACT_METHODS,
self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS,
self::GROUP_PUBLIC_STATIC_METHODS,
self::GROUP_PUBLIC_METHODS,
],
self::GROUP_SHORTCUT_PROTECTED_METHODS => [
self::GROUP_PROTECTED_FINAL_METHODS,
self::GROUP_PROTECTED_STATIC_FINAL_METHODS,
self::GROUP_PROTECTED_ABSTRACT_METHODS,
self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_STATIC_METHODS,
self::GROUP_PROTECTED_METHODS,
],
self::GROUP_SHORTCUT_PRIVATE_METHODS => [
self::GROUP_PRIVATE_STATIC_METHODS,
self::GROUP_PRIVATE_METHODS,
],
self::GROUP_SHORTCUT_FINAL_METHODS => [
self::GROUP_PUBLIC_FINAL_METHODS,
self::GROUP_PROTECTED_FINAL_METHODS,
self::GROUP_PUBLIC_STATIC_FINAL_METHODS,
self::GROUP_PROTECTED_STATIC_FINAL_METHODS,
],
self::GROUP_SHORTCUT_ABSTRACT_METHODS => [
self::GROUP_PUBLIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_ABSTRACT_METHODS,
self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS,
],
self::GROUP_SHORTCUT_STATIC_METHODS => [
self::GROUP_STATIC_CONSTRUCTORS,
self::GROUP_PUBLIC_STATIC_FINAL_METHODS,
self::GROUP_PROTECTED_STATIC_FINAL_METHODS,
self::GROUP_PUBLIC_STATIC_ABSTRACT_METHODS,
self::GROUP_PROTECTED_STATIC_ABSTRACT_METHODS,
self::GROUP_PUBLIC_STATIC_METHODS,
self::GROUP_PROTECTED_STATIC_METHODS,
self::GROUP_PRIVATE_STATIC_METHODS,
],
self::GROUP_SHORTCUT_METHODS => [
self::GROUP_SHORTCUT_FINAL_METHODS,
self::GROUP_SHORTCUT_ABSTRACT_METHODS,
self::GROUP_SHORTCUT_STATIC_METHODS,
self::GROUP_CONSTRUCTOR,
self::GROUP_DESTRUCTOR,
self::GROUP_PUBLIC_METHODS,
self::GROUP_PROTECTED_METHODS,
self::GROUP_PRIVATE_METHODS,
self::GROUP_MAGIC_METHODS,
],
];

/**
* @param array<int, string> $supportedGroups
* @return array<int, string>
*/
public function unpackShortcut(string $shortcut, array $supportedGroups): array
{
$groups = [];

foreach (self::SHORTCUTS[$shortcut] as $groupOrShortcut) {
if (in_array($groupOrShortcut, $supportedGroups, true)) {
$groups[] = $groupOrShortcut;
assertType('array{\'public final methods\', \'protected final methods\', \'public static final methods\', \'protected static final methods\'}', self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS]);
} elseif (
!array_key_exists($groupOrShortcut, self::SHORTCUTS)
) {
// Nothing
assertType('array{\'public final methods\', \'protected final methods\', \'public static final methods\', \'protected static final methods\'}', self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS]);
} else {
$groups = array_merge($groups, $this->unpackShortcut($groupOrShortcut, $supportedGroups));
assertType('array{\'public final methods\', \'protected final methods\', \'public static final methods\', \'protected static final methods\'}', self::SHORTCUTS[self::GROUP_SHORTCUT_FINAL_METHODS]);
}
}

return $groups;
}

}

23 changes: 23 additions & 0 deletions tests/PHPStan/Analyser/data/bug-7621-2.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php declare(strict_types = 1);

namespace Bug7621;

use function PHPStan\Testing\assertType;

class Foo
{
private const FOO = [ 'foo' => ['foo', 'bar'] ];

public function foo(): void
{
assertType('array{\'foo\', \'bar\'}', self::FOO['foo']);
$keys = [0, 1, 2];
foreach ($keys as $key) {
if (array_key_exists($key, self::FOO['foo'])) {
assertType('array{\'foo\', \'bar\'}', self::FOO['foo']);
} else {
assertType('array{0?: \'foo\', 1?: \'bar\'}&non-empty-array', self::FOO['foo']);
}
}
}
}
20 changes: 20 additions & 0 deletions tests/PHPStan/Analyser/data/bug-7621-3.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php declare(strict_types = 1);

namespace Bug7621;

use function PHPStan\Testing\assertType;

class Bar
{
private const FOO = [ 'foo' => ['foo', 'bar'] ];


/** @param 'foo'|'bar' $key */
public function foo(string $key): void
{
if (!array_key_exists($key, self::FOO)) {
assertType('array{}|array{foo: array{\'foo\', \'bar\'}}', self::FOO);
assertType('array{\'foo\', \'bar\'}', self::FOO['foo']);
}
}
}

0 comments on commit 7af6056

Please sign in to comment.