Skip to content

Commit

Permalink
Generic parsing for anonymous functions
Browse files Browse the repository at this point in the history
  • Loading branch information
klimick committed Aug 21, 2023
1 parent 357a0a3 commit a5a2bf6
Show file tree
Hide file tree
Showing 10 changed files with 267 additions and 25 deletions.
10 changes: 10 additions & 0 deletions src/Psalm/Internal/Type/ParseTree/AnonymousFunctionGenericTree.php
@@ -0,0 +1,10 @@
<?php

namespace Psalm\Internal\Type\ParseTree;

/**
* @internal
*/
class AnonymousFunctionGenericTree extends GenericTree
{
}
5 changes: 1 addition & 4 deletions src/Psalm/Internal/Type/ParseTree/TemplateAsTree.php
Expand Up @@ -11,12 +11,9 @@ class TemplateAsTree extends ParseTree
{
public string $param_name;

public string $as;

public function __construct(string $param_name, string $as, ?ParseTree $parent = null)
public function __construct(string $param_name, ?ParseTree $parent = null)
{
$this->param_name = $param_name;
$this->as = $as;
$this->parent = $parent;
}
}
54 changes: 46 additions & 8 deletions src/Psalm/Internal/Type/ParseTreeCreator.php
Expand Up @@ -3,6 +3,7 @@
namespace Psalm\Internal\Type;

use Psalm\Exception\TypeParseTreeException;
use Psalm\Internal\Type\ParseTree\AnonymousFunctionGenericTree;
use Psalm\Internal\Type\ParseTree\CallableParamTree;
use Psalm\Internal\Type\ParseTree\CallableTree;
use Psalm\Internal\Type\ParseTree\CallableWithReturnTypeTree;
Expand Down Expand Up @@ -95,6 +96,22 @@ public function create(): ParseTree

$this->current_leaf->terminated = true;

if ($this->current_leaf instanceof AnonymousFunctionGenericTree &&
$this->current_leaf->parent instanceof CallableTree
) {
$next_token = $this->t + 1 < $this->type_token_count
? $this->type_tokens[$this->t + 1][0] ?? null
: null;

if ($next_token !== '(') {
throw new TypeParseTreeException("Unexpected token '$next_token'. Expected '('.");
}

$this->t++;

$this->current_leaf = $this->current_leaf->parent;
}

break;

case '}':
Expand Down Expand Up @@ -141,7 +158,8 @@ public function create(): ParseTree

case 'is':
case 'as':
$this->handleIsOrAs($type_token);
case 'of':
$this->handleIsOrAsOrOf($type_token);
break;

default:
Expand Down Expand Up @@ -715,7 +733,7 @@ private function handleAmpersand(): void
}

/** @param array{0: string, 1: int, 2?: string} $type_token */
private function handleIsOrAs(array $type_token): void
private function handleIsOrAsOrOf(array $type_token): void
{
if ($this->t === 0) {
$this->handleValue($type_token);
Expand All @@ -726,24 +744,19 @@ private function handleIsOrAs(array $type_token): void
array_pop($current_parent->children);
}

if ($type_token[0] === 'as') {
$next_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null;

if ($type_token[0] === 'as' || $type_token[0] === 'of') {
if (!$this->current_leaf instanceof Value
|| !$current_parent instanceof GenericTree
|| !$next_token
) {
throw new TypeParseTreeException('Unexpected token ' . $type_token[0]);
}

$this->current_leaf = new TemplateAsTree(
$this->current_leaf->value,
$next_token[0],
$current_parent,
);

$current_parent->children[] = $this->current_leaf;
++$this->t;
} elseif ($this->current_leaf instanceof Value) {
$this->current_leaf = new TemplateIsTree(
$this->current_leaf->value,
Expand Down Expand Up @@ -771,6 +784,31 @@ private function handleValue(array $type_token): void

switch ($next_token[0] ?? null) {
case '<':
if (in_array(
$type_token[0],
['callable', 'pure-callable', 'Closure', '\Closure', 'pure-Closure'],
true,
)) {
$callable_parent = new CallableTree(
$type_token[0],
$new_parent,
);

if ($this->parse_tree instanceof Root) {
$this->parse_tree = $callable_parent;
$this->current_leaf = $callable_parent;
}

$new_leaf = new AnonymousFunctionGenericTree(
'anonymous-function-generics',
$callable_parent,
);

++$this->t;

break;
}

$new_leaf = new GenericTree(
$type_token[0],
$new_parent,
Expand Down
92 changes: 83 additions & 9 deletions src/Psalm/Internal/Type/TypeParser.php
Expand Up @@ -7,6 +7,7 @@
use Psalm\Codebase;
use Psalm\Exception\TypeParseTreeException;
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\Type\ParseTree\AnonymousFunctionGenericTree;
use Psalm\Internal\Type\ParseTree\CallableParamTree;
use Psalm\Internal\Type\ParseTree\CallableTree;
use Psalm\Internal\Type\ParseTree\CallableWithReturnTypeTree;
Expand Down Expand Up @@ -74,6 +75,7 @@
use function array_merge;
use function array_pop;
use function array_shift;
use function array_slice;
use function array_unique;
use function array_unshift;
use function array_values;
Expand Down Expand Up @@ -235,11 +237,19 @@ public static function getTypeFromTree(
throw new TypeParseTreeException('Invalid return type');
}

$anonymous_template_type_map = [];

foreach ($callable_type->templates ?? [] as $template_param) {
$anonymous_template_type_map[$template_param->param_name] = [
'anonymous-fn' => $template_param->as,
];
}

$return_type = self::getTypeFromTree(
$parse_tree->children[1],
$codebase,
null,
$template_type_map,
array_merge($template_type_map, $anonymous_template_type_map),
$type_aliases,
$from_docblock,
);
Expand Down Expand Up @@ -318,14 +328,26 @@ public static function getTypeFromTree(
}

if ($parse_tree instanceof TemplateAsTree) {
$result = new TTemplateParam(
if (!isset($parse_tree->children[0])) {
throw new TypeParseTreeException('TemplateAsTree does not have a child');
}

$tree_type = self::getTypeFromTree(
$parse_tree->children[0],
$codebase,
null,
$template_type_map,
$type_aliases,
$from_docblock,
);

return new TTemplateParam(
$parse_tree->param_name,
new Union([new TNamedObject($parse_tree->as)]),
$tree_type instanceof Union ? $tree_type : new Union([$tree_type]),
'class-string-map',
[],
$from_docblock,
);
return $result;
}

if ($parse_tree instanceof ConditionalTree) {
Expand Down Expand Up @@ -1212,8 +1234,58 @@ private static function getTypeFromCallableTree(
bool $from_docblock
) {
$params = [];
$templates = [];

foreach ($parse_tree->children as $child_tree) {
$generic_tree = null;

if (isset($parse_tree->children[0]) && $parse_tree->children[0] instanceof AnonymousFunctionGenericTree) {
$generic_tree = $parse_tree->children[0];
}

foreach (null !== $generic_tree ? $generic_tree->children : [] as $child_tree) {
if ($child_tree instanceof TemplateAsTree) {
if (!isset($child_tree->children[0])) {
throw new TypeParseTreeException('TemplateAsTree does not have a child');
}

$tree_type = self::getTypeFromTree(
$child_tree->children[0],
$codebase,
null,
$template_type_map,
$type_aliases,
$from_docblock,
);

$templates[] = new TTemplateParam(
$child_tree->param_name,
$tree_type instanceof Union ? $tree_type : new Union([$tree_type]),
'anonymous-fn',
);
} elseif ($child_tree instanceof Value) {
$templates[] = new TTemplateParam(
$child_tree->value,
Type::getMixed(),
'anonymous-fn',
);
} else {
throw new TypeParseTreeException('Unable to parse generics of anonymous function');
}
}

$anonymous_template_type_map = [];

foreach ($templates as $template_param) {
$anonymous_template_type_map[$template_param->param_name] = [
'anonymous-fn' => $template_param->as,
];
}

$children = null !== $generic_tree
? array_slice($parse_tree->children, 1)
: $parse_tree->children;

foreach ($children as $child_tree) {
$is_variadic = false;
$is_optional = false;

Expand All @@ -1223,7 +1295,7 @@ private static function getTypeFromCallableTree(
$child_tree->children[0],
$codebase,
null,
$template_type_map,
array_merge($template_type_map, $anonymous_template_type_map),
$type_aliases,
$from_docblock,
);
Expand All @@ -1242,7 +1314,7 @@ private static function getTypeFromCallableTree(
$child_tree,
$codebase,
null,
$template_type_map,
array_merge($template_type_map, $anonymous_template_type_map),
$type_aliases,
$from_docblock,
);
Expand All @@ -1265,11 +1337,13 @@ private static function getTypeFromCallableTree(

$pure = strpos($parse_tree->value, 'pure-') === 0 ? true : null;

$function_templates = $templates === [] ? null : $templates;

if (in_array(strtolower($parse_tree->value), ['closure', '\closure', 'pure-closure'], true)) {
return new TClosure('Closure', $params, null, $pure, [], [], $from_docblock);
return new TClosure('Closure', $params, null, $pure, [], [], $from_docblock, $function_templates);
}

return new TCallable('callable', $params, null, $pure, $from_docblock);
return new TCallable('callable', $params, null, $pure, $from_docblock, $function_templates);
}

/**
Expand Down
9 changes: 9 additions & 0 deletions src/Psalm/Internal/Type/TypeTokenizer.php
Expand Up @@ -152,6 +152,15 @@ public static function tokenize(string $string_type, bool $ignore_space = true):
$type_tokens[++$rtc] = ['', ++$i];
$was_char = false;
continue;
} elseif ($was_space
&& ($char === 'o')
&& ($chars[$i + 1] ?? null) === 'f'
&& ($chars[$i + 2] ?? null) === ' '
) {
$type_tokens[++$rtc] = [$char . 'f', $i - 1];
$type_tokens[++$rtc] = ['', ++$i];
$was_char = false;
continue;
} elseif ($was_char) {
$type_tokens[++$rtc] = ['', $i];
}
Expand Down
1 change: 1 addition & 0 deletions src/Psalm/Internal/TypeVisitor/TypeChecker.php
Expand Up @@ -349,6 +349,7 @@ public function checkTemplateParam(TTemplateParam $atomic): void
if ($this->prevent_template_covariance
&& strpos($atomic->defining_class, 'fn-') !== 0
&& $atomic->defining_class !== 'class-string-map'
&& $atomic->defining_class !== 'anonymous-fn'
) {
$codebase = $this->source->getCodebase();

Expand Down

0 comments on commit a5a2bf6

Please sign in to comment.