mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +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,
|
$type_extends->param_name,
|
||||||
array_keys($calling_class_storage->template_types)
|
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] = [
|
$class_template_params[$type_name] = [
|
||||||
$lhs_type_part->type_params[(int) $mapped_offset],
|
$lhs_type_part->type_params[(int) $mapped_offset],
|
||||||
$class_storage->name,
|
$class_storage->name,
|
||||||
@ -1073,6 +1092,8 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
|
|||||||
|
|
||||||
$i++;
|
$i++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//var_dump($class_template_params);
|
||||||
} else {
|
} else {
|
||||||
foreach ($class_storage->template_types as $type_name => list($type)) {
|
foreach ($class_storage->template_types as $type_name => list($type)) {
|
||||||
if ($class_storage !== $calling_class_storage
|
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 => $_) {
|
foreach ($storage->template_types as $template_name => $_) {
|
||||||
if (isset($found_generic_params[$template_name])) {
|
if (isset($found_generic_params[$template_name])) {
|
||||||
$generic_params[] = $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 {
|
} else {
|
||||||
$generic_params[] = [Type::getMixed(), null];
|
$generic_params[] = [Type::getMixed(), null];
|
||||||
}
|
}
|
||||||
@ -444,4 +450,38 @@ class NewAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\CallAna
|
|||||||
|
|
||||||
return null;
|
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;
|
$template_types = null;
|
||||||
|
|
||||||
if ($function_storage) {
|
if ($function_storage) {
|
||||||
$template_types = [];
|
$template_types = self::getTemplateTypesForFunction(
|
||||||
|
$function_storage,
|
||||||
if ($function_storage->template_types) {
|
$class_storage,
|
||||||
$template_types = $function_storage->template_types;
|
$calling_class_storage
|
||||||
}
|
);
|
||||||
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];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$existing_generic_params = $generic_params ?: [];
|
$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 StatementsAnalyzer $statements_analyzer
|
||||||
* @param array<int, PhpParser\Node\Arg> $args
|
* @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);
|
$storage->parent_classes = array_merge($storage->parent_classes, $parent_storage->parent_classes);
|
||||||
|
|
||||||
if ($parent_storage->template_types
|
if ($parent_storage->template_types
|
||||||
|
&& $parent_storage_class
|
||||||
&& isset($storage->template_type_extends[$parent_storage_class])
|
&& isset($storage->template_type_extends[$parent_storage_class])
|
||||||
) {
|
) {
|
||||||
foreach ($storage->template_type_extends[$parent_storage_class] as $i => $type) {
|
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;
|
$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);
|
$this->inheritMethodsFromParent($storage, $parent_storage);
|
||||||
|
@ -423,6 +423,10 @@ abstract class Type
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($parse_tree instanceof ParseTree\NullableTree) {
|
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);
|
$non_nullable_type = self::getTypeFromTree($parse_tree->children[0], false, $template_type_map);
|
||||||
|
|
||||||
if ($non_nullable_type instanceof Union) {
|
if ($non_nullable_type instanceof Union) {
|
||||||
|
@ -1672,6 +1672,96 @@ class TemplateTest extends TestCase
|
|||||||
'$id' => 'array-key',
|
'$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");',
|
$au = new AppUser("string");',
|
||||||
'error_message' => 'InvalidScalarArgument',
|
'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