1
0
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:
Brown 2019-01-10 16:59:44 -05:00
parent 3533339884
commit f108badd03
11 changed files with 445 additions and 73 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 {
/**

View File

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

View File

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

View File

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

View File

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