Skip to content
Draft
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
4 changes: 4 additions & 0 deletions src/DocBlockResolvers/Type/IdentifierTypeNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ public function resolve(Ast\Type\IdentifierTypeNode $node)
}
}

if (in_array($name, ['static', 'self'], true) && ($receiverType = $this->scope->getReceiverType())) {
return clone $receiverType;
}

return Type::from($this->scope->getUse($name));
}
}
44 changes: 44 additions & 0 deletions src/NodeResolvers/Shared/ResolvesClosureReturnTypes.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,48 @@ protected function resolveClosureReturnType(Node\Expr $expr): ?TypeContract

return null;
}

/**
* Resolve a closure's return type, injecting known types for untyped params.
* Used when the collection's template types (e.g. TValue=string) are available
* and should flow into the closure body.
*
* @param array<int, TypeContract|null> $paramTypes Resolved types indexed by param position
*/
protected function resolveClosureReturnTypeWithParamHints(Node\Expr $expr, array $paramTypes): ?TypeContract
{
if ($expr instanceof Node\Expr\ArrowFunction) {
if ($expr->returnType) {
return $this->from($expr->returnType);
}

foreach ($expr->params as $i => $param) {
if ($param->type === null && isset($paramTypes[$i]) && $paramTypes[$i] !== null) {
$this->scope->state()->add($param, $paramTypes[$i]);
}
}

return $this->from($expr->expr);
}

if ($expr instanceof Node\Expr\Closure) {
if ($expr->returnType) {
return $this->from($expr->returnType);
}

foreach ($expr->params as $i => $param) {
if ($param->type === null && isset($paramTypes[$i]) && $paramTypes[$i] !== null) {
$this->scope->state()->add($param, $paramTypes[$i]);
}
}

foreach ($expr->stmts as $stmt) {
if ($stmt instanceof Node\Stmt\Return_ && $stmt->expr !== null) {
return $this->from($stmt->expr);
}
}
}

return null;
}
}
53 changes: 51 additions & 2 deletions src/NodeResolvers/Shared/ResolvesMethodCalls.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,14 @@
use Laravel\Surveyor\Types\StringType;
use Laravel\Surveyor\Types\Type;
use PhpParser\Node;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr;

trait ResolvesMethodCalls
{
use AddsValidationRules, LazilyLoadsDependencies, ResolvesResourceConditionals;
use AddsValidationRules, LazilyLoadsDependencies, ResolvesClosureReturnTypes, ResolvesResourceConditionals;

protected function resolveMethodCall(Node\Expr\MethodCall|Node\Expr\NullsafeMethodCall $node)
protected function resolveMethodCall(Expr\MethodCall|Expr\NullsafeMethodCall $node)
{
$var = $this->from($node->var);

Expand Down Expand Up @@ -65,7 +67,54 @@ protected function resolveMethodCall(Node\Expr\MethodCall|Node\Expr\NullsafeMeth
$var,
$methodName->value,
$node,
$this->resolveCallableArgReturnTypes($node->args),
$this->getCallableArgNodes($node->args),
fn (Expr $expr, array $paramTypes) => $this->resolveClosureReturnTypeWithParamHints($expr, $paramTypes),
),
);
}

/**
* @param Arg[] $args
* @return array<int, Expr>
*/
protected function getCallableArgNodes(array $args): array
{
$nodes = [];

foreach ($args as $i => $arg) {
if (
$arg->value instanceof Expr\ArrowFunction
|| $arg->value instanceof Expr\Closure
) {
$nodes[$i] = $arg->value;
}
}

return $nodes;
}

/**
* @param Arg[] $args
* @return array<int, \Laravel\Surveyor\Types\Contracts\Type>
*/
protected function resolveCallableArgReturnTypes(array $args): array
{
$closureReturnTypes = [];

foreach ($args as $i => $arg) {
if (
$arg->value instanceof Expr\ArrowFunction
|| $arg->value instanceof Expr\Closure
) {
$returnType = $this->resolveClosureReturnType($arg->value);

if ($returnType !== null) {
$closureReturnTypes[$i] = $returnType;
}
}
}

return $closureReturnTypes;
}
}
112 changes: 110 additions & 2 deletions src/Parser/DocBlockParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@
// use Laravel\Surveyor\Types\Contracts\Type as TypeContract;
// use Laravel\Surveyor\Types\Type as RangerType;
use PhpParser\Node\Expr\CallLike;
use PHPStan\PhpDocParser\Ast\PhpDoc\ExtendsTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\MixinTagValueNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocNode;
use PHPStan\PhpDocParser\Ast\PhpDoc\UsesTagValueNode;
use PHPStan\PhpDocParser\Ast\Type\CallableTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Lexer\Lexer;
use PHPStan\PhpDocParser\Parser\ConstExprParser;
use PHPStan\PhpDocParser\Parser\PhpDocParser;
Expand Down Expand Up @@ -109,11 +112,18 @@ public function parseTemplateTags(string $docBlock): array
{
$this->parse($docBlock);

$templateTags = array_map(fn ($tag) => $this->resolve($tag), $this->parsed->getTemplateTagValues());
$regularTagValues = $this->parsed->getTemplateTagValues();
$covariantTagValues = array_map(
fn ($tag) => $tag->value,
$this->parsed->getTagsByName('@template-covariant'),
);
$allTagValues = array_merge($regularTagValues, $covariantTagValues);

$templateTags = array_map(fn ($tag) => $this->resolve($tag), $allTagValues);

$this->scope->setTemplateTags($templateTags);

return $this->parsed->getTemplateTagValues();
return $allTagValues;
}

public function parseProperties(string $docBlock): array
Expand Down Expand Up @@ -200,6 +210,104 @@ public function getTemplateTagNames(string $docBlock): array
return array_map(fn ($tag) => $tag->name, $this->parsed->getTemplateTagValues());
}

/**
* Return all template parameter names (both @template and @template-covariant),
* preserving document order.
*
* @return list<string>
*/
public function getAllTemplateTagNames(string $docBlock): array
{
if (! $docBlock) {
return [];
}

$this->parse($docBlock);

$regularNames = array_map(fn ($tag) => $tag->name, $this->parsed->getTemplateTagValues());
$covariantNames = array_map(
fn ($tag) => $tag->value->name,
$this->parsed->getTagsByName('@template-covariant'),
);

return array_merge($regularNames, $covariantNames);
}

/**
* Parse @extends Foo<T> tags and return their resolved generic ClassType objects.
*/
public function parseExtendsTags(string $docBlock): array
{
$this->parse($docBlock);

return array_map(
fn (ExtendsTagValueNode $node) => $this->resolve($node->type),
$this->parsed->getExtendsTagValues(),
);
}

/**
* For each @param whose type is callable(...): TXxx, return [paramName => templateName].
* Operates at the PHPStan AST level so it never triggers template resolution.
*
* @return array<string, string>
*/
public function getCallableParamReturnTemplates(string $docBlock): array
{
$this->parse($docBlock);

$result = [];

foreach ($this->parsed->getParamTagValues() as $tag) {
if (
$tag->type instanceof CallableTypeNode
&& $tag->type->returnType instanceof IdentifierTypeNode
) {
$paramName = ltrim($tag->parameterName, '$');
$result[$paramName] = $tag->type->returnType->name;
}
}

return $result;
}

/**
* For callable @param tags, returns the INPUT param type names keyed by param name.
* e.g. "@param callable(TValue, TKey): TMapValue $callback" → ['callback' => ['TValue', 'TKey']]
* Only IdentifierTypeNode params are included; complex types are represented as null.
*
* @return array<string, array<int, string|null>>
*/
public function getCallableParamInputTypeNames(string $docBlock): array
{
$this->parse($docBlock);

$result = [];

foreach ($this->parsed->getParamTagValues() as $tag) {
if (! $tag->type instanceof CallableTypeNode) {
continue;
}

$paramName = ltrim($tag->parameterName, '$');
$inputTypeNames = [];

foreach ($tag->type->parameters as $callableParam) {
if ($callableParam->type instanceof IdentifierTypeNode) {
$inputTypeNames[] = $callableParam->type->name;
} else {
$inputTypeNames[] = null;
}
}

if (! empty($inputTypeNames)) {
$result[$paramName] = $inputTypeNames;
}
}

return $result;
}

protected function parse(string $docBlock): PhpDocNode
{
if (isset($this->cached[$docBlock])) {
Expand Down
Loading
Loading