Bug Report
| Subject |
Details |
| Rector version |
last dev-main |
| Installed as |
composer dependency |
Minimal PHP Code Causing Issue
See https://getrector.com/demo/925bd1ba-642a-434c-9cfe-1a6191bfd328
<?php
namespace ReproC;
class V
{
public function one()
{
return $this->sink([["x" => 1], "s"]); // list mixing a string-keyed array with a scalar
}
public function two()
{
return $this->sink([[["d" => 1]]]); // same, nested one level deeper
}
protected function sink(array $data)
{
return $data;
}
}
Responsible rules
ClassMethodArrayDocblockParamFromLocalCallsRector
Expected Behavior
The union of the two call-site types, as Rector already produces correctly for every
neighbouring variant, e.g.
@param array<int, array<string, int>>|string[]|array<int, array<int, array<string, int>>> $data
Reduction notes
Both conditions are required; removing either yields a correct union:
one() |
two() |
result |
[["x" => 1], "s"] |
[[["d" => 1]]] |
array<mixed[], mixed> |
[["x" => 1], "s"] |
[[[1]]] |
correct union |
[[1], "s"] |
[[["d" => 1]]] |
correct union |
[["x" => 1]] |
[[["d" => 1]]] |
correct union |
| either literal alone |
— |
correct |
So it needs: a string-keyed innermost array in both, one call site mixing an array with a
non-array element, and the other nested one level deeper.
In our codebase this appeared as
@param array<array<int, array<string, mixed>>, mixed> $data on a method with ~55
heterogeneous callers — same defect, larger inputs.
Where it likely comes from
Not pinned to a line, but the strong suspect is
Rector\NodeTypeResolver\PHPStan\Type\TypeFactory::unwrapConstantArrayTypes(), which
flattens keys and values into two independent lists and pairs them by index:
$flattenKeyTypes = TypeUtils::flattenTypes($constantArrayType->getIterableKeyType());
$flattenItemTypes = TypeUtils::flattenTypes($constantArrayType->getIterableValueType());
foreach ($flattenItemTypes as $position => $nestedFlattenItemType) {
$nestedFlattenKeyType = $flattenKeyTypes[$position] ?? null;
// ...
$unwrappedTypes[] = new ArrayType($nestedFlattenKeyType, $nestedFlattenItemType);
}
The two lists have no guaranteed correspondence once shapes are heterogeneous — union and
dedup change their length and order independently. That pairing cannot by itself put an
array in the key slot, though, so the final malformed node is probably produced downstream
when the resulting union is generalized, in
NodeDocblockTypeDecorator::createTypeNode() (generalizeConstantTypes() then
StaticTypeMapper::mapPHPStanTypeToPHPStanPhpDocTypeNode()).
Worth noting the existing guard does not catch it: NodeDocblockTypeDecorator::isArrayMixed()
only rejects a mixed value type when the key is IntegerType, so a key that is an
ArrayType passes straight through and gets written.
Bug Report
Minimal PHP Code Causing Issue
See https://getrector.com/demo/925bd1ba-642a-434c-9cfe-1a6191bfd328
Responsible rules
ClassMethodArrayDocblockParamFromLocalCallsRectorExpected Behavior
The union of the two call-site types, as Rector already produces correctly for every
neighbouring variant, e.g.
Reduction notes
Both conditions are required; removing either yields a correct union:
one()two()[["x" => 1], "s"][[["d" => 1]]]array<mixed[], mixed>[["x" => 1], "s"][[[1]]][[1], "s"][[["d" => 1]]][["x" => 1]][[["d" => 1]]]So it needs: a string-keyed innermost array in both, one call site mixing an array with a
non-array element, and the other nested one level deeper.
In our codebase this appeared as
@param array<array<int, array<string, mixed>>, mixed> $dataon a method with ~55heterogeneous callers — same defect, larger inputs.
Where it likely comes from
Not pinned to a line, but the strong suspect is
Rector\NodeTypeResolver\PHPStan\Type\TypeFactory::unwrapConstantArrayTypes(), whichflattens keys and values into two independent lists and pairs them by index:
The two lists have no guaranteed correspondence once shapes are heterogeneous — union and
dedup change their length and order independently. That pairing cannot by itself put an
array in the key slot, though, so the final malformed node is probably produced downstream
when the resulting union is generalized, in
NodeDocblockTypeDecorator::createTypeNode()(generalizeConstantTypes()thenStaticTypeMapper::mapPHPStanTypeToPHPStanPhpDocTypeNode()).Worth noting the existing guard does not catch it:
NodeDocblockTypeDecorator::isArrayMixed()only rejects a mixed value type when the key is
IntegerType, so a key that is anArrayTypepasses straight through and gets written.