diff --git a/src/Rules/MissingTypehintCheck.php b/src/Rules/MissingTypehintCheck.php index 80b5e3e6ba8..704aca05db5 100644 --- a/src/Rules/MissingTypehintCheck.php +++ b/src/Rules/MissingTypehintCheck.php @@ -24,15 +24,16 @@ use PHPStan\Type\ObjectType; use PHPStan\Type\Type; use PHPStan\Type\TypeTraverser; -use PHPStan\Type\UnionType; use PHPStan\Type\VerbosityLevel; use Traversable; use function array_filter; use function array_keys; use function array_merge; +use function array_slice; use function count; use function implode; use function in_array; +use function spl_object_id; use function sprintf; use function strtolower; @@ -73,7 +74,8 @@ public function __construct( public function getIterableTypesWithMissingValueTypehint(Type $type): array { $descriptions = []; - TypeTraverser::map($type, function (Type $type, callable $traverse) use (&$descriptions): Type { + $sealed = []; + TypeTraverser::map($type, self::oncePerType($descriptions, function (Type $type, callable $traverse) use (&$descriptions, &$sealed): Type { if ($type instanceof TemplateType) { return $type; } @@ -108,19 +110,16 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array } if ($type->isIterable()->yes()) { if ($type->isConstantArray()->yes()) { - $type = TypeTraverser::map($type, static function (Type $type, callable $traverse) { - if ($type instanceof UnionType || $type instanceof IntersectionType) { - return $traverse($type); + $type = TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$sealed) { + $id = spl_object_id($type); + if (!isset($sealed[$id])) { + $sealedType = $type instanceof ConstantArrayType && $type->getUnsealedTypes() !== null + ? $type->dropUnsealedTypes() + : $type; + $sealed[$id] = [$type, $traverse($sealedType)]; } - if ($type instanceof ConstantArrayType) { - $unsealed = $type->getUnsealedTypes(); - if ($unsealed !== null) { - return $traverse($type->dropUnsealedTypes()); - } - } - - return $traverse($type); + return $sealed[$id][1]; }); } $iterableValue = $type->getIterableValueType(); @@ -136,11 +135,41 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array } } return $traverse($type); - }); + })); return $descriptions; } + /** + * A type alias used in several places is one shared object: check it once, then replay the descriptions it added. + * + * @param string[] $descriptions + * @param callable(Type, callable(Type): Type): Type $check + * @return callable(Type, callable(Type): Type): Type + */ + private static function oncePerType(array &$descriptions, callable $check): callable + { + $checked = []; + + return static function (Type $type, callable $traverse) use ($check, &$checked, &$descriptions): Type { + $id = spl_object_id($type); + if (isset($checked[$id])) { + [, $result, $added] = $checked[$id]; + foreach ($added as $description) { + $descriptions[] = $description; + } + + return $result; + } + + $count = count($descriptions); + $result = $check($type, $traverse); + $checked[$id] = [$type, $result, array_slice($descriptions, $count)]; + + return $result; + }; + } + /** * @return array */ diff --git a/tests/bench/data/nested-array-shape-alias.php b/tests/bench/data/nested-array-shape-alias.php new file mode 100644 index 00000000000..29138bc0cf9 --- /dev/null +++ b/tests/bench/data/nested-array-shape-alias.php @@ -0,0 +1,28 @@ +css(['p0' => 'a', 'p1' => 'b', 'c0' => ['p2' => 'c']]); +}