Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Analyser/Traverser/TransformStaticTypeTraverser.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@
use PHPStan\Turbo\ReferencedByTurboExtension;
use PHPStan\Type\StaticType;
use PHPStan\Type\ThisType;
use PHPStan\Type\Traverser\MemoizingTraverser;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverserCallable;

#[ReferencedByTurboExtension(key: 'transformStaticTypeTraverser')]
final class TransformStaticTypeTraverser implements TypeTraverserCallable
{

private MemoizingTraverser $memoizingTraverser;

public function __construct(
private readonly Scope $scope,
)
{
$this->memoizingTraverser = new MemoizingTraverser($this->doTraverse(...));
}

/**
Expand All @@ -27,6 +31,15 @@ public function traverse(Type $type, callable $traverse): Type
if (!$this->scope->isInClass()) {
return $type;
}

return $this->memoizingTraverser->traverse($type, $traverse);
}

/**
* @param callable(Type): Type $traverse
*/
private function doTraverse(Type $type, callable $traverse): Type
{
if ($type instanceof StaticType) {
$classReflection = $this->scope->getClassReflection();
$changedType = $type->changeBaseClass($classReflection);
Expand Down
5 changes: 3 additions & 2 deletions src/Rules/Classes/LocalTypeAliasesCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
use PHPStan\Type\CircularTypeAliasErrorType;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Traverser\MemoizingTraverser;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\VerbosityLevel;
Expand Down Expand Up @@ -349,7 +350,7 @@ private function isAliasNameValid(string $aliasName, ?NameScope $nameScope): boo
private function hasErrorType(Type $type, string $aliasName, array &$errors): bool
{
$foundError = false;
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type {
TypeTraverser::map($type, new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$errors, &$foundError, $aliasName): Type {
if ($foundError) {
return $type;
}
Expand All @@ -371,7 +372,7 @@ private function hasErrorType(Type $type, string $aliasName, array &$errors): bo
}

return $traverse($type);
});
}));

return $foundError;
}
Expand Down
5 changes: 3 additions & 2 deletions src/Rules/Generics/GenericObjectTypeCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use PHPStan\Type\Generic\TypeProjectionHelper;
use PHPStan\Type\Traverser\MemoizingTraverser;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\VerbosityLevel;
Expand Down Expand Up @@ -182,7 +183,7 @@ public function check(
private function getGenericTypes(Type $phpDocType): array
{
$genericObjectTypes = [];
TypeTraverser::map($phpDocType, static function (Type $type, callable $traverse) use (&$genericObjectTypes): Type {
TypeTraverser::map($phpDocType, new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$genericObjectTypes): Type {
if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) {
$resolvedType = TemplateTypeHelper::resolveToBounds($type);
if (!$resolvedType instanceof GenericObjectType && !$resolvedType instanceof GenericStaticType) {
Expand All @@ -194,7 +195,7 @@ private function getGenericTypes(Type $phpDocType): array
}
$traverse($type);
return $type;
});
}));

return $genericObjectTypes;
}
Expand Down
17 changes: 9 additions & 8 deletions src/Rules/MissingTypehintCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
use PHPStan\Type\IntersectionType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Traverser\MemoizingTraverser;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\UnionType;
Expand Down Expand Up @@ -73,7 +74,7 @@ public function __construct(
public function getIterableTypesWithMissingValueTypehint(Type $type): array
{
$descriptions = [];
TypeTraverser::map($type, function (Type $type, callable $traverse) use (&$descriptions): Type {
TypeTraverser::map($type, new MemoizingTraverser(function (Type $type, callable $traverse) use (&$descriptions): Type {
if ($type instanceof TemplateType) {
return $type;
}
Expand Down Expand Up @@ -108,7 +109,7 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array
}
if ($type->isIterable()->yes()) {
if ($type->isConstantArray()->yes()) {
$type = TypeTraverser::map($type, static function (Type $type, callable $traverse) {
$type = TypeTraverser::map($type, new MemoizingTraverser(static function (Type $type, callable $traverse): Type {
if ($type instanceof UnionType || $type instanceof IntersectionType) {
return $traverse($type);
}
Expand All @@ -121,7 +122,7 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array
}

return $traverse($type);
});
}));
}
$iterableValue = $type->getIterableValueType();
if ($iterableValue instanceof MixedType && !$iterableValue->isExplicitMixed()) {
Expand All @@ -136,7 +137,7 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array
}
}
return $traverse($type);
});
}));

return $descriptions;
}
Expand All @@ -147,7 +148,7 @@ public function getIterableTypesWithMissingValueTypehint(Type $type): array
public function getNonGenericObjectTypesWithGenericClass(Type $type): array
{
$objectTypes = [];
TypeTraverser::map($type, function (Type $type, callable $traverse) use (&$objectTypes): Type {
TypeTraverser::map($type, new MemoizingTraverser(function (Type $type, callable $traverse) use (&$objectTypes): Type {
if ($type instanceof GenericObjectType || $type instanceof GenericStaticType) {
$traverse($type);
return $type;
Expand Down Expand Up @@ -205,7 +206,7 @@ public function getNonGenericObjectTypesWithGenericClass(Type $type): array
}

return $traverse($type);
});
}));

return $objectTypes;
}
Expand All @@ -220,7 +221,7 @@ public function getCallablesWithMissingSignature(Type $type): array
}

$result = [];
TypeTraverser::map($type, static function (Type $type, callable $traverse) use (&$result): Type {
TypeTraverser::map($type, new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$result): Type {
if (
($type instanceof CallableType && $type->isCommonCallable())
|| ($type instanceof ClosureType && $type->isCommonCallable())
Expand All @@ -229,7 +230,7 @@ public function getCallablesWithMissingSignature(Type $type): array
$result[] = $type;
}
return $traverse($type);
});
}));

return $result;
}
Expand Down
21 changes: 11 additions & 10 deletions src/Rules/PhpDoc/ConditionalReturnTypeRuleHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PHPStan\Type\ConditionalTypeForParameter;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\StaticType;
use PHPStan\Type\Traverser\MemoizingTraverser;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\VerbosityLevel;
Expand All @@ -30,44 +31,44 @@ public function check(ExtendedParametersAcceptor $acceptor): array
$conditionalTypes = [];
$parametersByName = [];
foreach ($acceptor->getParameters() as $parameter) {
TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
TypeTraverser::map($parameter->getType(), new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
$conditionalTypes[] = $type;
}

return $traverse($type);
});
}));

if ($parameter->getOutType() !== null) {
TypeTraverser::map($parameter->getOutType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
TypeTraverser::map($parameter->getOutType(), new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
$conditionalTypes[] = $type;
}

return $traverse($type);
});
}));
}

if ($parameter->getClosureThisType() !== null) {
TypeTraverser::map($parameter->getClosureThisType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
TypeTraverser::map($parameter->getClosureThisType(), new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
$conditionalTypes[] = $type;
}

return $traverse($type);
});
}));
}

$parametersByName[$parameter->getName()] = $parameter;
}

TypeTraverser::map($acceptor->getReturnType(), static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
TypeTraverser::map($acceptor->getReturnType(), new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$conditionalTypes): Type {
if ($type instanceof ConditionalType || $type instanceof ConditionalTypeForParameter) {
$conditionalTypes[] = $type;
}

return $traverse($type);
});
}));

$errors = [];
foreach ($conditionalTypes as $conditionalType) {
Expand All @@ -77,14 +78,14 @@ public function check(ExtendedParametersAcceptor $acceptor): array
continue;
}
$templateTypes = [];
TypeTraverser::map($subjectType, static function (Type $type, callable $traverse) use (&$templateTypes): Type {
TypeTraverser::map($subjectType, new MemoizingTraverser(static function (Type $type, callable $traverse) use (&$templateTypes): Type {
if ($type instanceof TemplateType) {
$templateTypes[] = $type;
return $type;
}

return $traverse($type);
});
}));

if (count($templateTypes) === 0) {
$errors[] = RuleErrorBuilder::message(sprintf('Conditional return type uses subject type %s which is not part of PHPDoc @template tags.', $subjectType->describe(VerbosityLevel::typeOnly())))
Expand Down
5 changes: 3 additions & 2 deletions src/Rules/PhpDoc/GenericCallableRuleHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use PHPStan\Type\CallableType;
use PHPStan\Type\ClosureType;
use PHPStan\Type\Generic\TemplateTypeScope;
use PHPStan\Type\Traverser\MemoizingTraverser;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\VerbosityLevel;
Expand Down Expand Up @@ -46,7 +47,7 @@ public function check(
{
$errors = [];

TypeTraverser::map($callableType, function (Type $type, callable $traverse) use (&$errors, $node, $scope, $location, $functionName, $functionTemplateTags, $classReflection) {
TypeTraverser::map($callableType, new MemoizingTraverser(function (Type $type, callable $traverse) use (&$errors, $node, $scope, $location, $functionName, $functionTemplateTags, $classReflection) {
if (!($type instanceof CallableType || $type instanceof ClosureType)) {
return $traverse($type);
}
Expand Down Expand Up @@ -114,7 +115,7 @@ public function check(
}

return $traverse($type);
});
}));

return $errors;
}
Expand Down
9 changes: 5 additions & 4 deletions src/Rules/RuleLevelHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
use PHPStan\Type\StrictMixedType;
use PHPStan\Type\Traverser\MemoizingTraverser;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeTraverser;
Expand Down Expand Up @@ -86,7 +87,7 @@ private function transformCommonType(Type $type): Type
return $type;
}

return TypeTraverser::map($type, function (Type $type, callable $traverse) {
return TypeTraverser::map($type, new MemoizingTraverser(function (Type $type, callable $traverse) {
if ($type instanceof TemplateMixedType) {
if ($this->checkExplicitMixed) {
return $type->toStrictMixedType();
Expand All @@ -103,7 +104,7 @@ private function transformCommonType(Type $type): Type
}

return $traverse($type);
});
}));
}

/**
Expand All @@ -112,7 +113,7 @@ private function transformCommonType(Type $type): Type
private function transformAcceptedType(Type $acceptingType, Type $acceptedType): array
{
$checkForUnion = $this->checkUnionTypes;
$acceptedType = TypeTraverser::map($acceptedType, function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type {
$acceptedType = TypeTraverser::map($acceptedType, new MemoizingTraverser(function (Type $acceptedType, callable $traverse) use ($acceptingType, &$checkForUnion): Type {
if ($acceptedType instanceof CallableType) {
if ($acceptedType->isCommonCallable()) {
return $acceptedType;
Expand Down Expand Up @@ -175,7 +176,7 @@ private function transformAcceptedType(Type $acceptingType, Type $acceptedType):
}

return $traverse($acceptedType);
});
}));

return [$this->transformCommonType($acceptedType), $checkForUnion];
}
Expand Down
49 changes: 49 additions & 0 deletions src/Type/Traverser/MemoizingTraverser.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Traverser;

use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverserCallable;
use function spl_object_id;

/**
* Calls the wrapped callback only once per distinct Type object within a single
* TypeTraverser::map() call and reuses its result on repeated visits.
*
* Big array shapes built from type aliases share the same inner Type objects many
* times over, so a plain traversal visits orders of magnitude more nodes than there
* are distinct types. Only suitable for callbacks whose result depends on the visited
* type alone — callbacks collecting types into a list see each object once.
*/
final class MemoizingTraverser implements TypeTraverserCallable
{

/** @var array<int, array{Type, Type}> */
private array $results = [];

/**
* @param callable(Type $type, callable(Type): Type $traverse): Type $cb
*/
public function __construct(private $cb)
{
}

/**
* @param callable(Type): Type $traverse
*/
public function traverse(Type $type, callable $traverse): Type
{
$id = spl_object_id($type);
if (isset($this->results[$id])) {
return $this->results[$id][1];
}

$result = ($this->cb)($type, $traverse);

// keeps the visited type alive so that its object id is not reused
$this->results[$id] = [$type, $result];

return $result;
}

}
Loading
Loading