Skip to content

Commit

Permalink
Implement template default types
Browse files Browse the repository at this point in the history
Co-authored-by: Richard van Velzen <[email protected]>
Co-authored-by: Richard van Velzen <[email protected]>
  • Loading branch information
3 people authored Oct 9, 2024
1 parent c79b69a commit f9a2648
Show file tree
Hide file tree
Showing 83 changed files with 861 additions and 67 deletions.
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

0 comments on commit f9a2648

Please sign in to comment.