Skip to content

Implement template default types #3457

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Oct 9, 2024
6 changes: 6 additions & 0 deletions src/Analyser/MutatingScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -2539,6 +2539,7 @@ private function createFirstClassCallable(
$templateTags[$templateType->getName()] = new TemplateTag(
$templateType->getName(),
$templateType->getBound(),
$templateType->getDefault(),
$templateType->getVariance(),
);
}
Expand Down Expand Up @@ -5606,6 +5607,11 @@ private function exactInstantiation(New_ $node, string $className): ?Type
$list[] = $templateType;
continue;
}
$default = $tag->getDefault();
if ($default !== null) {
$list[] = $default;
continue;
}
$bound = $tag->getBound();
if ($bound instanceof MixedType && $bound->isExplicitMixed()) {
$bound = new MixedType(false);
Expand Down
11 changes: 11 additions & 0 deletions src/Dependency/DependencyResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,17 @@ private function addClassToDependencies(string $className, array &$dependenciesR
}
$dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass);
}

$default = $templateTag->getDefault();
if ($default === null) {
continue;
}
foreach ($default->getReferencedClasses() as $referencedClass) {
if (!$this->reflectionProvider->hasClass($referencedClass)) {
continue;
}
$dependenciesReflections[] = $this->reflectionProvider->getClass($referencedClass);
}
}

foreach ($classReflection->getPropertyTags() as $propertyTag) {
Expand Down
8 changes: 7 additions & 1 deletion src/PhpDoc/PhpDocNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,9 @@ public function resolveMethodTags(PhpDocNode $phpDocNode, NameScope $nameScope):
$templateType->bound !== null
? $this->typeNodeResolver->resolve($templateType->bound, $nameScope)
: new MixedType(),
$templateType->default !== null
? $this->typeNodeResolver->resolve($templateType->default, $nameScope)
: null,
TemplateTypeVariance::createInvariant(),
);
}
Expand Down Expand Up @@ -327,9 +330,12 @@ public function resolveTemplateTags(PhpDocNode $phpDocNode, NameScope $nameScope
}
}

$nameScopeWithoutCurrent = $nameScope->unsetTemplateType($valueNode->name);

$resolved[$valueNode->name] = new TemplateTag(
$valueNode->name,
$valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScope->unsetTemplateType($valueNode->name)) : new MixedType(true),
$valueNode->bound !== null ? $this->typeNodeResolver->resolve($valueNode->bound, $nameScopeWithoutCurrent) : new MixedType(true),
$valueNode->default !== null ? $this->typeNodeResolver->resolve($valueNode->default, $nameScopeWithoutCurrent) : null,
$variance,
);
$resolvedPrefix[$valueNode->name] = $prefix;
Expand Down
7 changes: 6 additions & 1 deletion src/PhpDoc/Tag/TemplateTag.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class TemplateTag
/**
* @param non-empty-string $name
*/
public function __construct(private string $name, private Type $bound, private TemplateTypeVariance $variance)
public function __construct(private string $name, private Type $bound, private ?Type $default, private TemplateTypeVariance $variance)
{
}

Expand All @@ -32,6 +32,11 @@ public function getBound(): Type
return $this->bound;
}

public function getDefault(): ?Type
{
return $this->default;
}

public function getVariance(): TemplateTypeVariance
{
return $this->variance;
Expand Down
13 changes: 13 additions & 0 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
use Traversable;
use function array_key_exists;
use function array_map;
use function array_values;
use function count;
use function explode;
use function get_class;
Expand Down Expand Up @@ -792,6 +793,15 @@ static function (string $variance): TemplateTypeVariance {

$classReflection = $this->getReflectionProvider()->getClass($mainTypeClassName);
if ($classReflection->isGeneric()) {
$templateTypes = array_values($classReflection->getTemplateTypeMap()->getTypes());
for ($i = count($genericTypes), $templateTypesCount = count($templateTypes); $i < $templateTypesCount; $i++) {
$templateType = $templateTypes[$i];
if (!$templateType instanceof TemplateType || $templateType->getDefault() === null) {
continue;
}
$genericTypes[] = $templateType->getDefault();
}

if (in_array($mainTypeClassName, [
Traversable::class,
IteratorAggregate::class,
Expand Down Expand Up @@ -910,6 +920,9 @@ private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $
$templateType->bound !== null
? $this->resolve($templateType->bound, $nameScope)
: new MixedType(),
$templateType->default !== null
? $this->resolve($templateType->default, $nameScope)
: null,
TemplateTypeVariance::createInvariant(),
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/Reflection/ClassReflection.php
Original file line number Diff line number Diff line change
Expand Up @@ -1442,7 +1442,7 @@ public function typeMapFromList(array $types): TemplateTypeMap
$map = [];
$i = 0;
foreach ($resolvedPhpDoc->getTemplateTags() as $tag) {
$map[$tag->getName()] = $types[$i] ?? $tag->getBound();
$map[$tag->getName()] = $types[$i] ?? $tag->getDefault() ?? $tag->getBound();
$i++;
}

Expand Down Expand Up @@ -1479,7 +1479,7 @@ public function typeMapToList(TemplateTypeMap $typeMap): array

$list = [];
foreach ($resolvedPhpDoc->getTemplateTags() as $tag) {
$list[] = $typeMap->getType($tag->getName()) ?? $tag->getBound();
$list[] = $typeMap->getType($tag->getName()) ?? $tag->getDefault() ?? $tag->getBound();
}

return $list;
Expand Down
3 changes: 1 addition & 2 deletions src/Rules/Classes/LocalTypeAliasesCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
use PHPStan\Type\VerbosityLevel;
use function array_key_exists;
use function array_merge;
use function implode;
use function in_array;
use function sprintf;

Expand Down Expand Up @@ -211,7 +210,7 @@ public function checkInTraitDefinitionContext(ClassReflection $reflection): arra
$reflection->getDisplayName(),
$aliasName,
$name,
implode(', ', $genericTypeNames),
$genericTypeNames,
))
->identifier('missingType.generics')
->build();
Expand Down
3 changes: 1 addition & 2 deletions src/Rules/Classes/MethodTagCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use function array_merge;
use function implode;
use function sprintf;

final class MethodTagCheck
Expand Down Expand Up @@ -174,7 +173,7 @@ private function checkMethodTypeInTraitDefinitionContext(ClassReflection $classR
$methodName,
$description,
$innerName,
implode(', ', $genericTypeNames),
$genericTypeNames,
))
->identifier('missingType.generics')
->build();
Expand Down
3 changes: 1 addition & 2 deletions src/Rules/Classes/MixinCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\VerbosityLevel;
use function array_merge;
use function implode;
use function sprintf;

final class MixinCheck
Expand Down Expand Up @@ -90,7 +89,7 @@ public function checkInTraitDefinitionContext(ClassReflection $classReflection):
$errors[] = RuleErrorBuilder::message(sprintf(
'PHPDoc tag @mixin contains generic %s but does not specify its types: %s',
$innerName,
implode(', ', $genericTypeNames),
$genericTypeNames,
))
->identifier('missingType.generics')
->build();
Expand Down
3 changes: 1 addition & 2 deletions src/Rules/Classes/PropertyTagCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use function array_merge;
use function implode;
use function sprintf;

final class PropertyTagCheck
Expand Down Expand Up @@ -155,7 +154,7 @@ private function checkPropertyTypeInTraitDefinitionContext(ClassReflection $clas
$classReflection->getDisplayName(),
$propertyName,
$innerName,
implode(', ', $genericTypeNames),
$genericTypeNames,
))
->identifier('missingType.generics')
->build();
Expand Down
3 changes: 1 addition & 2 deletions src/Rules/Constants/MissingClassConstantTypehintRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\VerbosityLevel;
use function array_merge;
use function implode;
use function sprintf;

/**
Expand Down Expand Up @@ -76,7 +75,7 @@ private function processSingleConstant(ClassReflection $classReflection, string
$constantReflection->getDeclaringClass()->getDisplayName(),
$constantName,
$name,
implode(', ', $genericTypeNames),
$genericTypeNames,
))
->identifier('missingType.generics')
->build();
Expand Down
4 changes: 2 additions & 2 deletions src/Rules/FunctionCallParametersCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -432,7 +432,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty
$type = $type->resolve();
}

if ($type instanceof TemplateType) {
if ($type instanceof TemplateType && $type->getDefault() === null) {
$returnTemplateTypes[$type->getName()] = true;
return $type;
}
Expand All @@ -444,7 +444,7 @@ static function (Type $type, callable $traverse) use (&$returnTemplateTypes): Ty
$parameterTemplateTypes = [];
foreach ($originalParametersAcceptor->getParameters() as $parameter) {
TypeTraverser::map($parameter->getType(), static function (Type $type, callable $traverse) use (&$parameterTemplateTypes): Type {
if ($type instanceof TemplateType) {
if ($type instanceof TemplateType && $type->getDefault() === null) {
$parameterTemplateTypes[$type->getName()] = true;
return $type;
}
Expand Down
3 changes: 1 addition & 2 deletions src/Rules/Functions/MissingFunctionParameterTypehintRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use function implode;
use function sprintf;

/**
Expand Down Expand Up @@ -100,7 +99,7 @@ private function checkFunctionParameter(FunctionReflection $functionReflection,
$functionReflection->getName(),
$parameterMessage,
$name,
implode(', ', $genericTypeNames),
$genericTypeNames,
))
->identifier('missingType.generics')
->build();
Expand Down
3 changes: 1 addition & 2 deletions src/Rules/Functions/MissingFunctionReturnTypehintRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\MixedType;
use PHPStan\Type\VerbosityLevel;
use function implode;
use function sprintf;

/**
Expand Down Expand Up @@ -58,7 +57,7 @@ public function processNode(Node $node, Scope $scope): array
'Function %s() return type with generic %s does not specify its types: %s',
$functionReflection->getName(),
$name,
implode(', ', $genericTypeNames),
$genericTypeNames,
))
->identifier('missingType.generics')
->build();
Expand Down
3 changes: 3 additions & 0 deletions src/Rules/Generics/ClassTemplateTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ public function processNode(Node $node, Scope $scope): array
sprintf('PHPDoc tag @template for %s cannot have existing type alias %%s as its name.', $displayName),
sprintf('PHPDoc tag @template %%s for %s has invalid bound type %%s.', $displayName),
sprintf('PHPDoc tag @template %%s for %s with bound type %%s is not supported.', $displayName),
sprintf('PHPDoc tag @template %%s for %s has invalid default type %%s.', $displayName),
sprintf('Default type %%s in PHPDoc tag @template %%s for %s is not subtype of bound type %%s.', $displayName),
sprintf('PHPDoc tag @template %%s for %s does not have a default type but follows an optional @template %%s.', $displayName),
);
}

Expand Down
3 changes: 3 additions & 0 deletions src/Rules/Generics/FunctionTemplateTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ public function processNode(Node $node, Scope $scope): array
sprintf('PHPDoc tag @template for function %s() cannot have existing type alias %%s as its name.', $escapedFunctionName),
sprintf('PHPDoc tag @template %%s for function %s() has invalid bound type %%s.', $escapedFunctionName),
sprintf('PHPDoc tag @template %%s for function %s() with bound type %%s is not supported.', $escapedFunctionName),
sprintf('PHPDoc tag @template %%s for function %s() has invalid default type %%s.', $escapedFunctionName),
sprintf('Default type %%s in PHPDoc tag @template %%s for function %s() is not subtype of bound type %%s.', $escapedFunctionName),
sprintf('PHPDoc tag @template %%s for function %s() does not have a default type but follows an optional @template %%s.', $escapedFunctionName),
);
}

Expand Down
16 changes: 15 additions & 1 deletion src/Rules/Generics/GenericAncestorsCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
use PHPStan\Rules\PhpDoc\UnresolvableTypeHelper;
use PHPStan\Rules\RuleErrorBuilder;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Generic\TypeProjectionHelper;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
use function array_fill_keys;
use function array_filter;
use function array_keys;
use function array_map;
use function array_merge;
Expand Down Expand Up @@ -173,10 +175,22 @@ public function check(
continue;
}

$templateTypes = $unusedNameClassReflection->getTemplateTypeMap()->getTypes();
$templateTypesCount = count($templateTypes);
$requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null));
if ($requiredTemplateTypesCount === 0) {
continue;
}

$templateTypesList = implode(', ', array_keys($templateTypes));
if ($requiredTemplateTypesCount !== $templateTypesCount) {
$templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount);
}

$messages[] = RuleErrorBuilder::message(sprintf(
$genericClassInNonGenericObjectType,
$unusedName,
implode(', ', array_keys($unusedNameClassReflection->getTemplateTypeMap()->getTypes())),
$templateTypesList,
))
->identifier('missingType.generics')
->build();
Expand Down
19 changes: 15 additions & 4 deletions src/Rules/Generics/GenericObjectTypeCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\VerbosityLevel;
use function array_filter;
use function array_keys;
use function array_values;
use function count;
Expand Down Expand Up @@ -59,27 +60,37 @@ public function check(
$genericTypeVariances = $genericType->getVariances();
$templateTypesCount = count($templateTypes);
$genericTypeTypesCount = count($genericTypeTypes);
if ($templateTypesCount > $genericTypeTypesCount) {
$requiredTemplateTypesCount = count(array_filter($templateTypes, static fn (Type $type) => $type instanceof TemplateType && $type->getDefault() === null));
if ($requiredTemplateTypesCount > $genericTypeTypesCount) {
$templateTypesList = implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes()));
if ($requiredTemplateTypesCount !== $templateTypesCount) {
$templateTypesList .= sprintf(' (%d-%d required).', $requiredTemplateTypesCount, $templateTypesCount);
}

$messages[] = RuleErrorBuilder::message(sprintf(
$notEnoughTypesMessage,
$genericType->describe(VerbosityLevel::typeOnly()),
$classLikeDescription,
$classReflection->getDisplayName(false),
implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())),
$templateTypesList,
))->identifier('generics.lessTypes')->build();
} elseif ($templateTypesCount < $genericTypeTypesCount) {
$templateTypesList = implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes()));
if ($requiredTemplateTypesCount !== $templateTypesCount) {
$templateTypesList .= sprintf(' (%d-%d required)', $requiredTemplateTypesCount, $templateTypesCount);
}

$messages[] = RuleErrorBuilder::message(sprintf(
$extraTypesMessage,
$genericType->describe(VerbosityLevel::typeOnly()),
$genericTypeTypesCount,
$classLikeDescription,
$classReflection->getDisplayName(false),
$templateTypesCount,
implode(', ', array_keys($classReflection->getTemplateTypeMap()->getTypes())),
$templateTypesList,
))->identifier('generics.moreTypes')->build();
}

$templateTypesCount = count($templateTypes);
for ($i = 0; $i < $templateTypesCount; $i++) {
if (!isset($genericTypeTypes[$i])) {
continue;
Expand Down
3 changes: 3 additions & 0 deletions src/Rules/Generics/InterfaceTemplateTypeRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ public function processNode(Node $node, Scope $scope): array
sprintf('PHPDoc tag @template for interface %s cannot have existing type alias %%s as its name.', $escapadInterfaceName),
sprintf('PHPDoc tag @template %%s for interface %s has invalid bound type %%s.', $escapadInterfaceName),
sprintf('PHPDoc tag @template %%s for interface %s with bound type %%s is not supported.', $escapadInterfaceName),
sprintf('PHPDoc tag @template %%s for interface %s has invalid default type %%s.', $escapadInterfaceName),
sprintf('Default type %%s in PHPDoc tag @template %%s for interface %s is not subtype of bound type %%s.', $escapadInterfaceName),
sprintf('PHPDoc tag @template %%s for interface %s does not have a default type but follows an optional @template %%s.', $escapadInterfaceName),
);
}

Expand Down
3 changes: 3 additions & 0 deletions src/Rules/Generics/MethodTagTemplateTypeCheck.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public function check(
sprintf('PHPDoc tag @method template for method %s::%s() cannot have existing type alias %%s as its name.', $escapedClassName, $escapedMethodName),
sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid bound type %%s.', $escapedClassName, $escapedMethodName),
sprintf('PHPDoc tag @method template %%s for method %s::%s() with bound type %%s is not supported.', $escapedClassName, $escapedMethodName),
sprintf('PHPDoc tag @method template %%s for method %s::%s() has invalid default type %%s', $escapedClassName, $escapedMethodName),
sprintf('Default type %%s in PHPDoc tag @method template %%s for method %s::%s() is not subtype of bound type %%s', $escapedClassName, $escapedMethodName),
sprintf('PHPDoc tag @template %%s for method %s::%s() does not have a default type but follows an optional @template %%s.', $escapedClassName, $escapedMethodName),
));

foreach (array_keys($methodTemplateTags) as $name) {
Expand Down
Loading
Loading