mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Make @template-extends more robust
This commit is contained in:
parent
a7005014c2
commit
9ef1ce1535
@ -1054,6 +1054,25 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
|
||||
$type_extends->param_name,
|
||||
array_keys($calling_class_storage->template_types)
|
||||
);
|
||||
$class_template_params[$type_name] = [
|
||||
$lhs_type_part->type_params[(int) $mapped_offset],
|
||||
$class_storage->name,
|
||||
];
|
||||
} elseif ($type_extends->defining_class
|
||||
&& isset(
|
||||
$calling_class_storage
|
||||
->template_type_extends
|
||||
[strtolower($type_extends->defining_class)]
|
||||
[$type_extends->param_name]
|
||||
)
|
||||
) {
|
||||
$mapped_offset = array_search(
|
||||
$type_extends->param_name,
|
||||
array_keys($calling_class_storage
|
||||
->template_type_extends
|
||||
[strtolower($type_extends->defining_class)])
|
||||
);
|
||||
|
||||
$class_template_params[$type_name] = [
|
||||
$lhs_type_part->type_params[(int) $mapped_offset],
|
||||
$class_storage->name,
|
||||
@ -1073,6 +1092,8 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
|
||||
|
||||
$i++;
|
||||
}
|
||||
|
||||
//var_dump($class_template_params);
|
||||
} else {
|
||||
foreach ($class_storage->template_types as $type_name => list($type)) {
|
||||
if ($class_storage !== $calling_class_storage
|
||||
|
@ -357,6 +357,12 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna
|
||||
foreach ($storage->template_types as $template_name => $_) {
|
||||
if (isset($found_generic_params[$template_name])) {
|
||||
$generic_params[] = $found_generic_params[$template_name];
|
||||
} elseif ($storage->template_type_extends && $found_generic_params) {
|
||||
$generic_params[] = self::getGenericParamForOffset(
|
||||
$template_name,
|
||||
$storage->template_type_extends,
|
||||
$found_generic_params
|
||||
);
|
||||
} else {
|
||||
$generic_params[] = [Type::getMixed(), null];
|
||||
}
|
||||
@ -444,4 +450,38 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $template_name
|
||||
* @param array<string, array<int|string, Type\Atomic>> $template_type_extends
|
||||
* @param array<string, array{Type\Union, ?string}> $found_generic_params
|
||||
* @return array{Type\Union, ?string}
|
||||
*/
|
||||
private static function getGenericParamForOffset(
|
||||
string $template_name,
|
||||
array $template_type_extends,
|
||||
array $found_generic_params
|
||||
) {
|
||||
if (isset($found_generic_params[$template_name])) {
|
||||
return $found_generic_params[$template_name];
|
||||
}
|
||||
|
||||
foreach ($template_type_extends as $type_map) {
|
||||
//var_dump($template_name, $type_map);
|
||||
foreach ($type_map as $extended_template_name => $extended_type) {
|
||||
if (is_string($extended_template_name)
|
||||
&& $extended_type instanceof Type\Atomic\TGenericParam
|
||||
&& $extended_type->param_name === $template_name
|
||||
) {
|
||||
return self::getGenericParamForOffset(
|
||||
$extended_template_name,
|
||||
$template_type_extends,
|
||||
$found_generic_params
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [Type::getMixed(), null];
|
||||
}
|
||||
}
|
||||
|
@ -869,35 +869,11 @@ class CallAnalyzer
|
||||
$template_types = null;
|
||||
|
||||
if ($function_storage) {
|
||||
$template_types = [];
|
||||
|
||||
if ($function_storage->template_types) {
|
||||
$template_types = $function_storage->template_types;
|
||||
}
|
||||
if ($class_storage) {
|
||||
if ($calling_class_storage
|
||||
&& $class_storage !== $calling_class_storage
|
||||
&& $calling_class_storage->template_type_extends
|
||||
) {
|
||||
foreach ($calling_class_storage->template_type_extends as $class_name_lc => $type_map) {
|
||||
foreach ($type_map as $template_name => $type) {
|
||||
if (is_string($template_name)
|
||||
&& $class_name_lc === strtolower($class_storage->name)
|
||||
) {
|
||||
$template_types[$template_name] = [new Type\Union([$type]), null];
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($class_storage->template_types) {
|
||||
foreach ($class_storage->template_types as $template_name => $type) {
|
||||
$template_types[$template_name] = $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($template_types as $key => $type) {
|
||||
$template_types[$key][0] = clone $type[0];
|
||||
}
|
||||
$template_types = self::getTemplateTypesForFunction(
|
||||
$function_storage,
|
||||
$class_storage,
|
||||
$calling_class_storage
|
||||
);
|
||||
}
|
||||
|
||||
$existing_generic_params = $generic_params ?: [];
|
||||
@ -1239,6 +1215,63 @@ class CallAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{Type\Union, ?string}>
|
||||
*/
|
||||
private static function getTemplateTypesForFunction(
|
||||
FunctionLikeStorage $function_storage,
|
||||
ClassLikeStorage $class_storage = null,
|
||||
ClassLikeStorage $calling_class_storage = null
|
||||
) : array {
|
||||
$template_types = [];
|
||||
|
||||
if ($function_storage->template_types) {
|
||||
$template_types = $function_storage->template_types;
|
||||
}
|
||||
|
||||
if ($class_storage) {
|
||||
if ($calling_class_storage
|
||||
&& $class_storage !== $calling_class_storage
|
||||
&& $calling_class_storage->template_type_extends
|
||||
) {
|
||||
foreach ($calling_class_storage->template_type_extends as $class_name_lc => $type_map) {
|
||||
foreach ($type_map as $template_name => $type) {
|
||||
if (is_string($template_name)
|
||||
&& $class_name_lc === strtolower($class_storage->name)
|
||||
) {
|
||||
if ($type instanceof Type\Atomic\TGenericParam
|
||||
&& $type->defining_class
|
||||
&& isset(
|
||||
$calling_class_storage
|
||||
->template_type_extends
|
||||
[strtolower($type->defining_class)]
|
||||
[$type->param_name]
|
||||
)
|
||||
) {
|
||||
$type = $calling_class_storage
|
||||
->template_type_extends
|
||||
[strtolower($type->defining_class)]
|
||||
[$type->param_name];
|
||||
}
|
||||
|
||||
$template_types[$template_name] = [new Type\Union([$type]), $class_storage->name];
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($class_storage->template_types) {
|
||||
foreach ($class_storage->template_types as $template_name => $type) {
|
||||
$template_types[$template_name] = $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($template_types as $key => $type) {
|
||||
$template_types[$key][0] = clone $type[0];
|
||||
}
|
||||
|
||||
return $template_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StatementsAnalyzer $statements_analyzer
|
||||
* @param array<int, PhpParser\Node\Arg> $args
|
||||
|
@ -334,6 +334,7 @@ class Populator
|
||||
$storage->parent_classes = array_merge($storage->parent_classes, $parent_storage->parent_classes);
|
||||
|
||||
if ($parent_storage->template_types
|
||||
&& $parent_storage_class
|
||||
&& isset($storage->template_type_extends[$parent_storage_class])
|
||||
) {
|
||||
foreach ($storage->template_type_extends[$parent_storage_class] as $i => $type) {
|
||||
@ -345,6 +346,13 @@ class Populator
|
||||
$storage->template_type_extends[$parent_storage_class][$mapped_name] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($parent_storage->template_type_extends) {
|
||||
$storage->template_type_extends = array_merge(
|
||||
$parent_storage->template_type_extends,
|
||||
$storage->template_type_extends
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->inheritMethodsFromParent($storage, $parent_storage);
|
||||
|
@ -423,6 +423,10 @@ abstract class Type
|
||||
}
|
||||
|
||||
if ($parse_tree instanceof ParseTree\NullableTree) {
|
||||
if (!isset($parse_tree->children[0])) {
|
||||
throw new TypeParseTreeException('Misplaced question mark');
|
||||
}
|
||||
|
||||
$non_nullable_type = self::getTypeFromTree($parse_tree->children[0], false, $template_type_map);
|
||||
|
||||
if ($non_nullable_type instanceof Union) {
|
||||
|
@ -1672,6 +1672,96 @@ class TemplateTest extends TestCase
|
||||
'$id' => 'array-key',
|
||||
]
|
||||
],
|
||||
'extendsTwiceSameName' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
class Container
|
||||
{
|
||||
/**
|
||||
* @var T
|
||||
*/
|
||||
private $v;
|
||||
/**
|
||||
* @param T $v
|
||||
*/
|
||||
public function __construct($v)
|
||||
{
|
||||
$this->v = $v;
|
||||
}
|
||||
/**
|
||||
* @return T
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template-extends Container<T>
|
||||
*/
|
||||
class ChildContainer extends Container {}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template-extends ChildContainer<T>
|
||||
*/
|
||||
class GrandChildContainer extends ChildContainer {}
|
||||
|
||||
$fc = new GrandChildContainer(5);
|
||||
$a = $fc->getValue();',
|
||||
[
|
||||
'$a' => 'int',
|
||||
]
|
||||
],
|
||||
'extendsTwiceDifferentName' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template T1
|
||||
*/
|
||||
class Container
|
||||
{
|
||||
/**
|
||||
* @var T1
|
||||
*/
|
||||
private $v;
|
||||
/**
|
||||
* @param T1 $v
|
||||
*/
|
||||
public function __construct($v)
|
||||
{
|
||||
$this->v = $v;
|
||||
}
|
||||
/**
|
||||
* @return T1
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T2
|
||||
* @template-extends Container<T2>
|
||||
*/
|
||||
class ChildContainer extends Container {}
|
||||
|
||||
/**
|
||||
* @template T3
|
||||
* @template-extends ChildContainer<T3>
|
||||
*/
|
||||
class GrandChildContainer extends ChildContainer {}
|
||||
|
||||
$fc = new GrandChildContainer(5);
|
||||
$a = $fc->getValue();',
|
||||
[
|
||||
'$a' => 'int',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -2051,6 +2141,132 @@ class TemplateTest extends TestCase
|
||||
$au = new AppUser("string");',
|
||||
'error_message' => 'InvalidScalarArgument',
|
||||
],
|
||||
'extendsTwiceDifferentNameBrokenChain' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template T1
|
||||
*/
|
||||
class Container
|
||||
{
|
||||
/**
|
||||
* @var T1
|
||||
*/
|
||||
private $v;
|
||||
/**
|
||||
* @param T1 $v
|
||||
*/
|
||||
public function __construct($v)
|
||||
{
|
||||
$this->v = $v;
|
||||
}
|
||||
/**
|
||||
* @return T1
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T2
|
||||
*/
|
||||
class ChildContainer extends Container {}
|
||||
|
||||
/**
|
||||
* @template T3
|
||||
* @template-extends ChildContainer<T3>
|
||||
*/
|
||||
class GrandChildContainer extends ChildContainer {}
|
||||
|
||||
$fc = new GrandChildContainer(5);
|
||||
$a = $fc->getValue();',
|
||||
'error_message' => 'MixedAssignment',
|
||||
],
|
||||
'extendsTwiceSameNameBrokenChain' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
class Container
|
||||
{
|
||||
/**
|
||||
* @var T
|
||||
*/
|
||||
private $v;
|
||||
/**
|
||||
* @param T $v
|
||||
*/
|
||||
public function __construct($v)
|
||||
{
|
||||
$this->v = $v;
|
||||
}
|
||||
/**
|
||||
* @return T
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
class ChildContainer extends Container {}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template-extends ChildContainer<T>
|
||||
*/
|
||||
class GrandChildContainer extends ChildContainer {}
|
||||
|
||||
$fc = new GrandChildContainer(5);
|
||||
$a = $fc->getValue();',
|
||||
'error_message' => 'MixedAssignment',
|
||||
],
|
||||
'extendsTwiceSameNameLastDoesNotExtend' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
class Container
|
||||
{
|
||||
/**
|
||||
* @var T
|
||||
*/
|
||||
private $v;
|
||||
/**
|
||||
* @param T $v
|
||||
*/
|
||||
public function __construct($v)
|
||||
{
|
||||
$this->v = $v;
|
||||
}
|
||||
/**
|
||||
* @return T
|
||||
*/
|
||||
public function getValue()
|
||||
{
|
||||
return $this->v;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template-extends Container<T>
|
||||
*/
|
||||
class ChildContainer extends Container {}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
class GrandChildContainer extends ChildContainer {}
|
||||
|
||||
$fc = new GrandChildContainer(5);
|
||||
$a = $fc->getValue();',
|
||||
'error_message' => 'MixedAssignment',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user