1
0
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:
Matthew Brown 2019-01-12 18:18:23 -05:00
parent a7005014c2
commit 9ef1ce1535
6 changed files with 351 additions and 29 deletions

View File

@ -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

View File

@ -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];
}
} }

View File

@ -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

View File

@ -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);

View File

@ -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) {

View File

@ -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',
],
]; ];
} }
} }