Skip to content

Array with invalid key in doc from ClassMethodArrayDocblockParamFromLocalCallsRector #9865

Description

@LorisZ

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions