Skip to content

Commit

Permalink
Add support for generic CallableType.
Browse files Browse the repository at this point in the history
  • Loading branch information
mad-briller committed Feb 23, 2024
1 parent 3639e4f commit abaea93
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 6 deletions.
51 changes: 47 additions & 4 deletions src/PhpDoc/TypeNodeResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use PhpParser\Node\Name;
use PHPStan\Analyser\ConstantResolver;
use PHPStan\Analyser\NameScope;
use PHPStan\PhpDoc\Tag\TemplateTag;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprArrayNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFalseNode;
use PHPStan\PhpDocParser\Ast\ConstExpr\ConstExprFloatNode;
Expand Down Expand Up @@ -66,7 +67,12 @@
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeFactory;
use PHPStan\Type\Generic\TemplateTypeHelper;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeScope;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
use PHPStan\Type\Helper\GetTemplateTypeType;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\IntegerType;
Expand Down Expand Up @@ -874,29 +880,66 @@ static function (string $variance): TemplateTypeVariance {
private function resolveCallableTypeNode(CallableTypeNode $typeNode, NameScope $nameScope): Type
{
$mainType = $this->resolve($typeNode->identifier, $nameScope);

$templateTags = [];

if (count($typeNode->templateTypes) > 0) {
foreach ($typeNode->templateTypes as $templateType) {
$templateTags[$templateType->name] = new TemplateTag(
$templateType->name,
$templateType->bound !== null
? $this->resolve($templateType->bound, $nameScope)
: new MixedType(),
TemplateTypeVariance::createInvariant(),
);
}
$templateTypeScope = TemplateTypeScope::createWithAnonymousFunction();

$templateTypeMap = new TemplateTypeMap(array_map(
static fn (TemplateTag $tag): Type => TemplateTypeFactory::fromTemplateTag($templateTypeScope, $tag),
$templateTags
));

$nameScope = $nameScope->withTemplateTypeMap($templateTypeMap);
} else {
$templateTypeMap = TemplateTypeMap::createEmpty();
}

$isVariadic = false;
$parameters = array_map(
function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadic): NativeParameterReflection {
function (CallableTypeParameterNode $parameterNode) use ($nameScope, &$isVariadic, $templateTypeMap): NativeParameterReflection {
$isVariadic = $isVariadic || $parameterNode->isVariadic;
$parameterName = $parameterNode->parameterName;
if (str_starts_with($parameterName, '$')) {
$parameterName = substr($parameterName, 1);
}

return new NativeParameterReflection(
$parameterName,
$parameterNode->isOptional || $parameterNode->isVariadic,
$this->resolve($parameterNode->type, $nameScope),
TemplateTypeHelper::resolveTemplateTypes(
$this->resolve($parameterNode->type, $nameScope),
$templateTypeMap,
TemplateTypeVarianceMap::createEmpty(),
TemplateTypeVariance::createInvariant()
),
$parameterNode->isReference ? PassedByReference::createCreatesNewVariable() : PassedByReference::createNo(),
$parameterNode->isVariadic,
null,
);
},
$typeNode->parameters,
);
$returnType = $this->resolve($typeNode->returnType, $nameScope);

$returnType = TemplateTypeHelper::resolveTemplateTypes(
$this->resolve($typeNode->returnType, $nameScope),
$templateTypeMap,
TemplateTypeVarianceMap::createEmpty(),
TemplateTypeVariance::createInvariant()
);

if ($mainType instanceof CallableType) {
return new CallableType($parameters, $returnType, $isVariadic);
return new CallableType($parameters, $returnType, $isVariadic, $templateTypeMap);

} elseif (
$mainType instanceof ObjectType
Expand Down
12 changes: 10 additions & 2 deletions src/Type/CallableType.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ class CallableType implements CompoundType, ParametersAcceptor

private bool $isCommonCallable;

private TemplateTypeMap $templateTypeMap;

private TemplateTypeMap $resolvedTemplateTypeMap;

/**
* @api
* @param array<int, ParameterReflection>|null $parameters
Expand All @@ -62,11 +66,15 @@ public function __construct(
?array $parameters = null,
?Type $returnType = null,
private bool $variadic = true,
?TemplateTypeMap $templateTypeMap = null,
?TemplateTypeMap $resolvedTemplateTypeMap = null,
)
{
$this->parameters = $parameters ?? [];
$this->returnType = $returnType ?? new MixedType();
$this->isCommonCallable = $parameters === null && $returnType === null;
$this->templateTypeMap = $templateTypeMap ?? TemplateTypeMap::createEmpty();
$this->resolvedTemplateTypeMap = $resolvedTemplateTypeMap ?? TemplateTypeMap::createEmpty();
}

/**
Expand Down Expand Up @@ -243,12 +251,12 @@ public function toArrayKey(): Type

public function getTemplateTypeMap(): TemplateTypeMap
{
return TemplateTypeMap::createEmpty();
return $this->templateTypeMap;
}

public function getResolvedTemplateTypeMap(): TemplateTypeMap
{
return TemplateTypeMap::createEmpty();
return $this->resolvedTemplateTypeMap;
}

public function getCallSiteVarianceMap(): TemplateTypeVarianceMap
Expand Down
9 changes: 9 additions & 0 deletions src/Type/Generic/TemplateTypeScope.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
class TemplateTypeScope
{

public static function createWithAnonymousFunction(): self
{
return new self(null, null);
}

public static function createWithFunction(string $functionName): self
{
return new self(null, $functionName);
Expand Down Expand Up @@ -48,6 +53,10 @@ public function equals(self $other): bool
/** @api */
public function describe(): string
{
if ($this->className === null && $this->functionName === null) {
return 'anonymous function';
}

if ($this->className === null) {
return sprintf('function %s()', $this->functionName);
}
Expand Down
1 change: 1 addition & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public function dataFileAsserts(): iterable
require_once __DIR__ . '/data/bug2574.php';

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug2574.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/generic-callables.php');

require_once __DIR__ . '/data/bug2577.php';

Expand Down
14 changes: 14 additions & 0 deletions tests/PHPStan/Analyser/data/generic-callables.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace GenericCallables;

use function PHPStan\Testing\assertType;

/**
* @param callable<TRet of mixed>(TRet $val): TRet $callable
*/
function test(callable $callable, int $int, string $str): void
{
assertType('int', $callable($int));
//assertType('string', $callable($str));
}

0 comments on commit abaea93

Please sign in to comment.