diff --git a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php index 7e54a2cb15..b8c0c2497a 100644 --- a/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php +++ b/src/Rules/Arrays/DuplicateKeysInLiteralArraysRule.php @@ -9,7 +9,9 @@ use PHPStan\Rules\Rule; use PHPStan\Rules\RuleErrorBuilder; use PHPStan\Type\Constant\ConstantIntegerType; +use function array_key_first; use function array_keys; +use function array_search; use function count; use function implode; use function is_int; @@ -36,7 +38,6 @@ public function getNodeType(): string public function processNode(Node $node, Scope $scope): array { - $values = []; $duplicateKeys = []; $printedValues = []; $valueLines = []; @@ -49,6 +50,8 @@ public function processNode(Node $node, Scope $scope): array * - False means a non-scalar value was encountered and we cannot be sure of the next keys. */ $autoGeneratedIndex = null; + $seenKeys = []; + $seenUnions = []; foreach ($node->getItemNodes() as $itemNode) { $item = $itemNode->getArrayItem(); if ($item === null) { @@ -84,6 +87,44 @@ public function processNode(Node $node, Scope $scope): array continue; } + $duplicate = false; + $newValues = $keyValues; + foreach ($newValues as $k => $newValue) { + if (array_search($newValue, $seenKeys, true) !== false) { + unset($newValues[$k]); + } + + if ($newValues === []) { + $duplicate = true; + break; + } + } + + if ($newValues !== []) { + if (count($newValues) === 1) { + $newValue = $newValues[array_key_first($newValues)]; + foreach ($seenUnions as $k => $union) { + $offset = array_search($newValue, $union, true); + if ($offset === false) { + continue; + } + + unset($seenUnions[$k][$offset]); + + // turn a union into a seen key, when all its elements have been seen + if (count($seenUnions[$k]) !== 1) { + continue; + } + + $seenKeys[] = $seenUnions[$k][array_key_first($seenUnions[$k])]; + unset($seenUnions[$k]); + } + $seenKeys[] = $newValue; + } else { + $seenUnions[] = $newValues; + } + } + foreach ($keyValues as $value) { $printedValue = $key !== null ? $this->exprPrinter->printExpr($key) @@ -94,9 +135,7 @@ public function processNode(Node $node, Scope $scope): array $valueLines[$value] = $item->getStartLine(); } - $previousCount = count($values); - $values[$value] = $printedValue; - if ($previousCount !== count($values)) { + if (!$duplicate) { continue; } diff --git a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php index 6d775a2f7c..a01e02424d 100644 --- a/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php +++ b/tests/PHPStan/Rules/Arrays/DuplicateKeysInLiteralArraysRuleTest.php @@ -73,7 +73,37 @@ public function testDuplicateKeys(): void 'Array has 2 duplicate keys with value \'key\' (\'key\', $key2).', 105, ], + [ + "Array has 2 duplicate keys with value 'bar' (\$key, 'bar').", + 128, + ], + [ + "Array has 2 duplicate keys with value 'bar' (\$key, 'bar').", + 139, + ], + [ + "Array has 2 duplicate keys with value 'foo' ('foo', \$key).", + 151, + ], + [ + "Array has 2 duplicate keys with value 'bar' ('bar', \$key).", + 152, + ], + [ + "Array has 2 duplicate keys with value 'baz' (\$key, 'baz').", + 171, + ], ]); } + public function testBug13013(): void + { + $this->analyse([__DIR__ . '/data/bug-13013.php'], []); + } + + public function testBug13022(): void + { + $this->analyse([__DIR__ . '/data/bug-13022.php'], []); + } + } diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13013.php b/tests/PHPStan/Rules/Arrays/data/bug-13013.php new file mode 100644 index 0000000000..dd5180869c --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-13013.php @@ -0,0 +1,77 @@ +> + */ + public array $mailsGroupedByTemplate; + + /** + * @var array> + */ + protected array $mailCounts; + + private function __construct() + { + $this->mailsGroupedByTemplate = []; + $this->mailCounts = []; + } + + public function countMailStates(): void + { + foreach ($this->mailsGroupedByTemplate as $templateId => $mails) { + $this->mailCounts[$templateId] = [ + MailStatus::notActive()->code => 0, + MailStatus::simulation()->code => 0, + MailStatus::active()->code => 0, + ]; + } + } +} + +final class MailStatus +{ + private const CODE_NOT_ACTIVE = 0; + + private const CODE_SIMULATION = 1; + + private const CODE_ACTIVE = 2; + + /** + * @var self::CODE_* + */ + public int $code; + + public string $name; + + public string $description; + + /** + * @param self::CODE_* $status + */ + public function __construct(int $status, string $name, string $description) + { + $this->code = $status; + $this->name = $name; + $this->description = $description; + } + + public static function notActive(): self + { + return new self(self::CODE_NOT_ACTIVE, _('Pausiert'), _('Es findet kein Mailversand an Kunden statt')); + } + + public static function simulation(): self + { + return new self(self::CODE_SIMULATION, _('Simulation'), _('Wenn Template zugewiesen, werden im Simulationsmodus E-Mails nur in der Datenbank gespeichert und nicht an den Kunden gesendet')); + } + + public static function active(): self + { + return new self(self::CODE_ACTIVE, _('Aktiv'), _('Wenn Template zugewiesen, findet Mailversand an Kunden statt')); + } + +} diff --git a/tests/PHPStan/Rules/Arrays/data/bug-13022.php b/tests/PHPStan/Rules/Arrays/data/bug-13022.php new file mode 100644 index 0000000000..22727c110a --- /dev/null +++ b/tests/PHPStan/Rules/Arrays/data/bug-13022.php @@ -0,0 +1,29 @@ + $object->getId(), + $targetId => 'info', + ]; + + // sql()->insert('tablename', $array); - example how this will be used +} diff --git a/tests/PHPStan/Rules/Arrays/data/duplicate-keys.php b/tests/PHPStan/Rules/Arrays/data/duplicate-keys.php index 06dbbeb0f5..7a73af9da9 100644 --- a/tests/PHPStan/Rules/Arrays/data/duplicate-keys.php +++ b/tests/PHPStan/Rules/Arrays/data/duplicate-keys.php @@ -107,4 +107,69 @@ public function doUnionKeys(string $key): void ]; } + /** + * @param 'foo'|'bar' $key + */ + public function maybeDuplicate(string $key): void + { + $a = [ + 'foo' => 'foo', + $key => 'foo|bar', + ]; + } + + /** + * @param 'foo'|'bar' $key + */ + public function sureDuplicate(string $key): void + { + $a = [ + 'foo' => 'foo', + $key => 'foo|bar', + 'bar' => 'bar', + ]; + } + + /** + * @param 'foo'|'bar' $key + */ + public function sureDuplicate2(string $key): void + { + $a = [ + $key => 'foo|bar', + 'foo' => 'foo', + 'bar' => 'bar', + ]; + } + + /** + * @param 'foo'|'bar' $key + */ + public function sureDuplicate3(string $key): void + { + $a = [ + 'foo' => 'foo', + 'bar' => 'bar', + $key => 'foo|bar', + ]; + } + + /** + * @param 'foo'|'bar'|'baz' $key + */ + public function sureDuplicate4(string $key): void + { + $a = [ + 'foo' => 'foo', + 'bar' => 'bar', + $key => 'foo|bar|baz', + ]; + + $b = [ + 'foo' => 'foo', + 'bar' => 'bar', + $key => 'foo|bar|baz', + 'baz' => 'baz', + ]; + } }