mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
Fix #1067 - add support for @template-extends
This commit is contained in:
parent
3533339884
commit
f108badd03
@ -103,6 +103,7 @@
|
||||
<referencedMethod name="Psalm\Codebase::getAppearingMethodId"/>
|
||||
<referencedMethod name="Psalm\Codebase::getOverriddenMethodIds"/>
|
||||
<referencedMethod name="Psalm\Codebase::getCasedMethodId"/>
|
||||
<referencedMethod name="Psalm\Codebase::createClassLikeStorage"/>
|
||||
<referencedMethod name="Psalm\Codebase::isVariadic"/>
|
||||
<referencedMethod name="Psalm\Codebase::getMethodReturnsByRef"/>
|
||||
</errorLevel>
|
||||
|
@ -529,7 +529,13 @@ class CommentAnalyzer
|
||||
|
||||
if (isset($comments['specials']['template-extends'])) {
|
||||
foreach ($comments['specials']['template-extends'] as $template_line) {
|
||||
$info->template_parents[] = $template_line;
|
||||
$info->template_extends[] = $template_line;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($comments['specials']['template-implements'])) {
|
||||
foreach ($comments['specials']['template-implements'] as $template_line) {
|
||||
$info->template_extends[] = $template_line;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -641,7 +641,8 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
|
||||
|
||||
if ($class_template_params) {
|
||||
$return_type_candidate->replaceTemplateTypesWithArgTypes(
|
||||
$class_template_params
|
||||
$class_template_params,
|
||||
$codebase
|
||||
);
|
||||
}
|
||||
} else {
|
||||
@ -722,7 +723,8 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
|
||||
|
||||
if ($class_template_params) {
|
||||
$return_type_candidate->replaceTemplateTypesWithArgTypes(
|
||||
$class_template_params
|
||||
$class_template_params,
|
||||
$codebase
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -333,6 +333,23 @@ class Populator
|
||||
|
||||
$storage->parent_classes = array_merge($storage->parent_classes, $parent_storage->parent_classes);
|
||||
|
||||
if (isset($storage->template_extends[$parent_storage_class])) {
|
||||
$i = 0;
|
||||
|
||||
foreach ($storage->template_extends[$parent_storage_class] as $template_name => $_) {
|
||||
if ($parent_storage->template_types) {
|
||||
$parent_template_type_names = array_keys($parent_storage->template_types);
|
||||
|
||||
if (isset($parent_template_type_names[$i])) {
|
||||
$storage->template_extends[$parent_storage_class][$template_name]
|
||||
= $parent_template_type_names[$i];
|
||||
}
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
$this->inheritMethodsFromParent($storage, $parent_storage);
|
||||
$this->inheritPropertiesFromParent($storage, $parent_storage);
|
||||
|
||||
@ -462,6 +479,23 @@ class Populator
|
||||
$implemented_interface_storage->invalid_dependencies
|
||||
);
|
||||
|
||||
if (isset($storage->template_extends[$implemented_interface_lc])) {
|
||||
$i = 0;
|
||||
|
||||
if ($implemented_interface_storage->template_types) {
|
||||
foreach ($storage->template_extends[$implemented_interface_lc] as $template_name => $_) {
|
||||
$parent_template_type_names = array_keys($implemented_interface_storage->template_types);
|
||||
|
||||
if (isset($parent_template_type_names[$i])) {
|
||||
$storage->template_extends[$implemented_interface_lc][$template_name]
|
||||
= $parent_template_type_names[$i];
|
||||
}
|
||||
}
|
||||
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
|
||||
$extra_interfaces = array_merge($extra_interfaces, $implemented_interface_storage->parent_interfaces);
|
||||
}
|
||||
|
||||
|
@ -111,6 +111,14 @@ class ClassLikeStorageProvider
|
||||
self::$storage = array_merge($more, self::$storage);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public function makeNew(string $fq_classlike_name_lc)
|
||||
{
|
||||
self::$new_storage[$fq_classlike_name_lc] = self::$storage[$fq_classlike_name_lc];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $fq_classlike_name
|
||||
*
|
||||
|
@ -28,7 +28,7 @@ class ClassLikeDocblockComment
|
||||
/**
|
||||
* @var array<int, string>
|
||||
*/
|
||||
public $template_parents = [];
|
||||
public $template_extends = [];
|
||||
|
||||
/**
|
||||
* @var array<int, array{name:string, type:string, tag:string, line_number:int}>
|
||||
|
@ -17,7 +17,7 @@ interface Traversable {
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @template-extends Traversable
|
||||
* @template-extends Traversable<TKey, TValue>
|
||||
*/
|
||||
interface IteratorAggregate extends Traversable {
|
||||
|
||||
@ -39,7 +39,7 @@ interface IteratorAggregate extends Traversable {
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @template-extends Traversable
|
||||
* @template-extends Traversable<TKey, TValue>
|
||||
*/
|
||||
interface Iterator extends Traversable {
|
||||
|
||||
@ -91,7 +91,7 @@ interface Iterator extends Traversable {
|
||||
* @template TSend
|
||||
* @template TReturn
|
||||
*
|
||||
* @template-extends Traversable
|
||||
* @template-implements Traversable<TKey, TValue>
|
||||
*/
|
||||
class Generator implements Traversable {
|
||||
/**
|
||||
|
@ -681,6 +681,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
) {
|
||||
// we're overwriting some methods
|
||||
$storage = $duplicate_storage;
|
||||
$this->codebase->classlike_storage_provider->makeNew(strtolower($fq_classlike_name));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -692,7 +693,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
$this->fq_classlike_names[] = $fq_classlike_name;
|
||||
|
||||
if (!$storage) {
|
||||
$storage = $this->codebase->createClassLikeStorage($fq_classlike_name);
|
||||
$storage = $this->codebase->classlike_storage_provider->create($fq_classlike_name);
|
||||
}
|
||||
|
||||
$storage->location = $class_location;
|
||||
@ -703,6 +704,51 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
|
||||
$this->classlike_storages[] = $storage;
|
||||
|
||||
if ($node instanceof PhpParser\Node\Stmt\Class_) {
|
||||
$storage->abstract = (bool)$node->isAbstract();
|
||||
$storage->final = (bool)$node->isFinal();
|
||||
|
||||
$this->codebase->classlikes->addFullyQualifiedClassName($fq_classlike_name, $this->file_path);
|
||||
|
||||
if ($node->extends) {
|
||||
$parent_fqcln = ClassLikeAnalyzer::getFQCLNFromNameObject($node->extends, $this->aliases);
|
||||
$this->codebase->scanner->queueClassLikeForScanning(
|
||||
$parent_fqcln,
|
||||
$this->file_path,
|
||||
$this->scan_deep
|
||||
);
|
||||
$parent_fqcln_lc = strtolower($parent_fqcln);
|
||||
$storage->parent_classes[$parent_fqcln_lc] = $parent_fqcln_lc;
|
||||
$this->file_storage->required_classes[strtolower($parent_fqcln)] = $parent_fqcln;
|
||||
}
|
||||
|
||||
foreach ($node->implements as $interface) {
|
||||
$interface_fqcln = ClassLikeAnalyzer::getFQCLNFromNameObject($interface, $this->aliases);
|
||||
$this->codebase->scanner->queueClassLikeForScanning($interface_fqcln, $this->file_path);
|
||||
$storage->class_implements[strtolower($interface_fqcln)] = $interface_fqcln;
|
||||
$this->file_storage->required_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
|
||||
}
|
||||
} elseif ($node instanceof PhpParser\Node\Stmt\Interface_) {
|
||||
$storage->is_interface = true;
|
||||
$this->codebase->classlikes->addFullyQualifiedInterfaceName($fq_classlike_name, $this->file_path);
|
||||
|
||||
foreach ($node->extends as $interface) {
|
||||
$interface_fqcln = ClassLikeAnalyzer::getFQCLNFromNameObject($interface, $this->aliases);
|
||||
$this->codebase->scanner->queueClassLikeForScanning($interface_fqcln, $this->file_path);
|
||||
$storage->parent_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
|
||||
$this->file_storage->required_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
|
||||
}
|
||||
} elseif ($node instanceof PhpParser\Node\Stmt\Trait_) {
|
||||
$storage->is_trait = true;
|
||||
$this->file_storage->has_trait = true;
|
||||
$this->codebase->classlikes->addFullyQualifiedTraitName($fq_classlike_name, $this->file_path);
|
||||
$this->codebase->classlikes->addTraitNode(
|
||||
$fq_classlike_name,
|
||||
$node,
|
||||
$this->aliases
|
||||
);
|
||||
}
|
||||
|
||||
if ($doc_comment) {
|
||||
$docblock_info = null;
|
||||
try {
|
||||
@ -730,15 +776,31 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
$template_name = $template_type[0];
|
||||
if (count($template_type) === 3) {
|
||||
if (trim($template_type[2])) {
|
||||
$storage->template_types[$template_name] = [
|
||||
Type::parseTokens(
|
||||
try {
|
||||
$template_type = Type::parseTokens(
|
||||
Type::fixUpLocalType(
|
||||
$template_type[2],
|
||||
$this->aliases,
|
||||
null,
|
||||
$this->type_aliases
|
||||
)
|
||||
),
|
||||
);
|
||||
} catch (TypeParseTreeException $e) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
$e->getMessage() . ' in docblock for '
|
||||
. implode('.', $this->fq_classlike_names),
|
||||
new CodeLocation($this->file_scanner, $node, null, true)
|
||||
)
|
||||
)) {
|
||||
}
|
||||
|
||||
$storage->has_docblock_issues = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$storage->template_types[$template_name] = [
|
||||
$template_type,
|
||||
$fq_classlike_name
|
||||
];
|
||||
} else {
|
||||
@ -757,11 +819,101 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
|
||||
$this->class_template_types = $storage->template_types;
|
||||
|
||||
if ($docblock_info->template_parents) {
|
||||
$storage->template_parents = [];
|
||||
foreach ($docblock_info->template_extends as $extended_class_name) {
|
||||
try {
|
||||
$extended_union_type = Type::parseTokens(
|
||||
Type::fixUpLocalType(
|
||||
$extended_class_name,
|
||||
$this->aliases,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
)
|
||||
);
|
||||
} catch (TypeParseTreeException $e) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
$e->getMessage() . ' in docblock for ' . implode('.', $this->fq_classlike_names),
|
||||
new CodeLocation($this->file_scanner, $node, null, true)
|
||||
)
|
||||
)) {
|
||||
}
|
||||
|
||||
foreach ($docblock_info->template_parents as $template_parent) {
|
||||
$storage->template_parents[$template_parent] = $template_parent;
|
||||
$storage->has_docblock_issues = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$extended_union_type->isSingle()) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'@template-extends cannot be a union type',
|
||||
new CodeLocation($this->file_scanner, $node, null, true)
|
||||
)
|
||||
)) {
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($extended_union_type->getTypes() as $atomic_type) {
|
||||
if (!$atomic_type instanceof Type\Atomic\TGenericObject) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'@template-extends has invalid class ' . $atomic_type->getId(),
|
||||
new CodeLocation($this->file_scanner, $node, null, true)
|
||||
)
|
||||
)) {
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (!isset($storage->parent_classes[strtolower($atomic_type->value)])
|
||||
&& !isset($storage->parent_interfaces[strtolower($atomic_type->value)])
|
||||
&& !isset($storage->class_implements[strtolower($atomic_type->value)])
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'@template-extends must include the name of an extended class,'
|
||||
. ' got ' . $atomic_type->getId(),
|
||||
new CodeLocation($this->file_scanner, $node, null, true)
|
||||
)
|
||||
)) {
|
||||
}
|
||||
}
|
||||
|
||||
$extended_type_parameters = [];
|
||||
|
||||
foreach ($atomic_type->type_params as $type_param) {
|
||||
if (!$type_param->isSingle()) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'@template-extends type parameter cannot be a union type',
|
||||
new CodeLocation($this->file_scanner, $node, null, true)
|
||||
)
|
||||
)) {
|
||||
}
|
||||
break 2;
|
||||
}
|
||||
|
||||
$extended_type_parameter = (string) $type_param;
|
||||
|
||||
if (!isset($this->class_template_types[$extended_type_parameter])) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'@template-extends type parameter ' . $extended_type_parameter
|
||||
. ' is not recognized',
|
||||
new CodeLocation($this->file_scanner, $node, null, true)
|
||||
)
|
||||
)) {
|
||||
}
|
||||
|
||||
break 2;
|
||||
}
|
||||
|
||||
$extended_type_parameters[$extended_type_parameter] = null;
|
||||
}
|
||||
|
||||
if ($extended_type_parameters) {
|
||||
$storage->template_extends[strtolower($atomic_type->value)] = $extended_type_parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -821,51 +973,6 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
}
|
||||
}
|
||||
|
||||
if ($node instanceof PhpParser\Node\Stmt\Class_) {
|
||||
$storage->abstract = (bool)$node->isAbstract();
|
||||
$storage->final = (bool)$node->isFinal();
|
||||
|
||||
$this->codebase->classlikes->addFullyQualifiedClassName($fq_classlike_name, $this->file_path);
|
||||
|
||||
if ($node->extends) {
|
||||
$parent_fqcln = ClassLikeAnalyzer::getFQCLNFromNameObject($node->extends, $this->aliases);
|
||||
$this->codebase->scanner->queueClassLikeForScanning(
|
||||
$parent_fqcln,
|
||||
$this->file_path,
|
||||
$this->scan_deep
|
||||
);
|
||||
$parent_fqcln_lc = strtolower($parent_fqcln);
|
||||
$storage->parent_classes[$parent_fqcln_lc] = $parent_fqcln_lc;
|
||||
$this->file_storage->required_classes[strtolower($parent_fqcln)] = $parent_fqcln;
|
||||
}
|
||||
|
||||
foreach ($node->implements as $interface) {
|
||||
$interface_fqcln = ClassLikeAnalyzer::getFQCLNFromNameObject($interface, $this->aliases);
|
||||
$this->codebase->scanner->queueClassLikeForScanning($interface_fqcln, $this->file_path);
|
||||
$storage->class_implements[strtolower($interface_fqcln)] = $interface_fqcln;
|
||||
$this->file_storage->required_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
|
||||
}
|
||||
} elseif ($node instanceof PhpParser\Node\Stmt\Interface_) {
|
||||
$storage->is_interface = true;
|
||||
$this->codebase->classlikes->addFullyQualifiedInterfaceName($fq_classlike_name, $this->file_path);
|
||||
|
||||
foreach ($node->extends as $interface) {
|
||||
$interface_fqcln = ClassLikeAnalyzer::getFQCLNFromNameObject($interface, $this->aliases);
|
||||
$this->codebase->scanner->queueClassLikeForScanning($interface_fqcln, $this->file_path);
|
||||
$storage->parent_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
|
||||
$this->file_storage->required_interfaces[strtolower($interface_fqcln)] = $interface_fqcln;
|
||||
}
|
||||
} elseif ($node instanceof PhpParser\Node\Stmt\Trait_) {
|
||||
$storage->is_trait = true;
|
||||
$this->file_storage->has_trait = true;
|
||||
$this->codebase->classlikes->addFullyQualifiedTraitName($fq_classlike_name, $this->file_path);
|
||||
$this->codebase->classlikes->addTraitNode(
|
||||
$fq_classlike_name,
|
||||
$node,
|
||||
$this->aliases
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($node->stmts as $node_stmt) {
|
||||
if ($node_stmt instanceof PhpParser\Node\Stmt\ClassConst) {
|
||||
$this->visitClassConstDeclaration($node_stmt, $storage, $fq_classlike_name);
|
||||
|
@ -266,9 +266,9 @@ class ClassLikeStorage
|
||||
public $template_types;
|
||||
|
||||
/**
|
||||
* @var array<string, string>|null
|
||||
* @var array<string, array<string, ?string>>|null
|
||||
*/
|
||||
public $template_parents;
|
||||
public $template_extends;
|
||||
|
||||
/**
|
||||
* @var array<string, array<int, CodeLocation>>|null
|
||||
|
@ -876,11 +876,9 @@ class Union
|
||||
$classlike_storage =
|
||||
$codebase->classlike_storage_provider->get($atomic_input_type->value);
|
||||
|
||||
if ($classlike_storage->template_parents
|
||||
&& in_array($atomic_type->value, $classlike_storage->template_parents)
|
||||
) {
|
||||
if (isset($classlike_storage->template_extends[strtolower($key)])) {
|
||||
$matching_atomic_type = $atomic_input_type;
|
||||
break;
|
||||
break;
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// do nothing
|
||||
@ -914,7 +912,7 @@ class Union
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function replaceTemplateTypesWithArgTypes(array $template_types)
|
||||
public function replaceTemplateTypesWithArgTypes(array $template_types, Codebase $codebase = null)
|
||||
{
|
||||
$keys_to_unset = [];
|
||||
|
||||
@ -925,10 +923,40 @@ class Union
|
||||
foreach ($this->types as $key => $atomic_type) {
|
||||
if ($atomic_type instanceof Type\Atomic\TGenericParam) {
|
||||
$keys_to_unset[] = $key;
|
||||
$template_type = isset($template_types[$key])
|
||||
&& $atomic_type->defining_class === $template_types[$key][1]
|
||||
? clone $template_types[$key][0]
|
||||
: Type::getMixed();
|
||||
|
||||
$template_type = null;
|
||||
|
||||
if (isset($template_types[$key]) && $atomic_type->defining_class === $template_types[$key][1]) {
|
||||
$template_type = clone $template_types[$key][0];
|
||||
} elseif ($codebase && $atomic_type->defining_class) {
|
||||
foreach ($template_types as $replacement_key => $template_type_map) {
|
||||
if (!$template_type_map[1]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$classlike_storage =
|
||||
$codebase->classlike_storage_provider->get($template_type_map[1]);
|
||||
|
||||
if ($classlike_storage->template_extends) {
|
||||
foreach ($classlike_storage->template_extends as $fq_class_name_lc => $param_map) {
|
||||
$param_map_reversed = array_flip($param_map);
|
||||
if (strtolower($atomic_type->defining_class) === $fq_class_name_lc
|
||||
&& isset($param_map_reversed[$key])
|
||||
&& isset($template_types[$param_map_reversed[$key]])
|
||||
) {
|
||||
$template_type = clone $template_types[$param_map_reversed[$key]][0];
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$template_type) {
|
||||
$template_type = Type::getMixed();
|
||||
}
|
||||
|
||||
foreach ($template_type->types as $template_type_part) {
|
||||
if ($template_type_part instanceof Type\Atomic\TMixed) {
|
||||
|
@ -638,7 +638,7 @@ class TemplateTest extends TestCase
|
||||
takesCollectionOfItems($c->map(function(Item $i): Item { return $i;}));
|
||||
takesCollectionOfItems($c->map(function(Item $i): Item { return $i;}));'
|
||||
],
|
||||
'replaceChildType' => [
|
||||
'replaceChildTypeWithGenerator' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template TKey as array-key
|
||||
@ -1274,7 +1274,7 @@ class TemplateTest extends TestCase
|
||||
if ($val) {}
|
||||
}',
|
||||
],
|
||||
'mixedTemplatedParamOutWithNoTemplateExtends' => [
|
||||
'mixedTemplatedParamOutWithNoExtendedTemplate' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template TValue
|
||||
@ -1336,6 +1336,192 @@ class TemplateTest extends TestCase
|
||||
],
|
||||
'error_levels' => ['MixedAssignment'],
|
||||
],
|
||||
'mixedTemplatedParamOutDifferentParamName' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template TValue
|
||||
*/
|
||||
class ValueContainer
|
||||
{
|
||||
/**
|
||||
* @var TValue
|
||||
*/
|
||||
private $v;
|
||||
/**
|
||||
* @param TValue $v
|
||||
*/
|
||||
public function __construct($v)
|
||||
{
|
||||
$this->v = $v;
|
||||
}
|
||||
/**
|
||||
* @return TValue
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TKey
|
||||
* @template Tv
|
||||
*/
|
||||
class KeyValueContainer extends ValueContainer
|
||||
{
|
||||
/**
|
||||
* @var TKey
|
||||
*/
|
||||
private $k;
|
||||
/**
|
||||
* @param TKey $k
|
||||
* @param Tv $v
|
||||
*/
|
||||
public function __construct($k, $v)
|
||||
{
|
||||
$this->k = $k;
|
||||
parent::__construct($v);
|
||||
}
|
||||
/**
|
||||
* @return TKey
|
||||
*/
|
||||
public function getKey()
|
||||
{
|
||||
return $this->k;
|
||||
}
|
||||
}
|
||||
$a = new KeyValueContainer("hello", 15);
|
||||
$b = $a->getValue();',
|
||||
[
|
||||
'$a' => 'KeyValueContainer<string, int>',
|
||||
'$b' => 'mixed'
|
||||
],
|
||||
'error_levels' => ['MixedAssignment'],
|
||||
],
|
||||
'templateExtendsSameName' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template TValue
|
||||
*/
|
||||
class ValueContainer
|
||||
{
|
||||
/**
|
||||
* @var TValue
|
||||
*/
|
||||
private $v;
|
||||
/**
|
||||
* @param TValue $v
|
||||
*/
|
||||
public function __construct($v)
|
||||
{
|
||||
$this->v = $v;
|
||||
}
|
||||
/**
|
||||
* @return TValue
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
* @template-extends ValueContainer<TValue>
|
||||
*/
|
||||
class KeyValueContainer extends ValueContainer
|
||||
{
|
||||
/**
|
||||
* @var TKey
|
||||
*/
|
||||
private $k;
|
||||
/**
|
||||
* @param TKey $k
|
||||
* @param TValue $v
|
||||
*/
|
||||
public function __construct($k, $v)
|
||||
{
|
||||
$this->k = $k;
|
||||
parent::__construct($v);
|
||||
}
|
||||
/**
|
||||
* @return TKey
|
||||
*/
|
||||
public function getKey()
|
||||
{
|
||||
return $this->k;
|
||||
}
|
||||
}
|
||||
$a = new KeyValueContainer("hello", 15);
|
||||
$b = $a->getValue();',
|
||||
[
|
||||
'$a' => 'KeyValueContainer<string, int>',
|
||||
'$b' => 'int'
|
||||
],
|
||||
],
|
||||
'templateExtendsDifferentName' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template TValue
|
||||
*/
|
||||
class ValueContainer
|
||||
{
|
||||
/**
|
||||
* @var TValue
|
||||
*/
|
||||
private $v;
|
||||
/**
|
||||
* @param TValue $v
|
||||
*/
|
||||
public function __construct($v)
|
||||
{
|
||||
$this->v = $v;
|
||||
}
|
||||
/**
|
||||
* @return TValue
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template TKey
|
||||
* @template Tv
|
||||
* @template-extends ValueContainer<Tv>
|
||||
*/
|
||||
class KeyValueContainer extends ValueContainer
|
||||
{
|
||||
/**
|
||||
* @var TKey
|
||||
*/
|
||||
private $k;
|
||||
/**
|
||||
* @param TKey $k
|
||||
* @param Tv $v
|
||||
*/
|
||||
public function __construct($k, $v)
|
||||
{
|
||||
$this->k = $k;
|
||||
parent::__construct($v);
|
||||
}
|
||||
/**
|
||||
* @return TKey
|
||||
*/
|
||||
public function getKey()
|
||||
{
|
||||
return $this->k;
|
||||
}
|
||||
}
|
||||
$a = new KeyValueContainer("hello", 15);
|
||||
$b = $a->getValue();',
|
||||
[
|
||||
'$a' => 'KeyValueContainer<string, int>',
|
||||
'$b' => 'int'
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user