mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Fix #1072 - add support for @use SomeTrait<T>
This commit is contained in:
parent
f67bab6d52
commit
081ba4b204
@ -211,7 +211,15 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
true
|
||||
);
|
||||
|
||||
$this->checkTemplateParams($codebase, $storage, $parent_class_storage, $code_location);
|
||||
if ($storage->template_type_extends_count !== null) {
|
||||
$this->checkTemplateParams(
|
||||
$codebase,
|
||||
$storage,
|
||||
$parent_class_storage,
|
||||
$code_location,
|
||||
$storage->template_type_extends_count
|
||||
);
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// do nothing
|
||||
}
|
||||
@ -259,7 +267,17 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
true
|
||||
);
|
||||
|
||||
$this->checkTemplateParams($codebase, $storage, $interface_storage, $code_location);
|
||||
if (isset($storage->template_type_implements_count[strtolower($fq_interface_name)])) {
|
||||
$expected_param_count = $storage->template_type_implements_count[strtolower($fq_interface_name)];
|
||||
|
||||
$this->checkTemplateParams(
|
||||
$codebase,
|
||||
$storage,
|
||||
$interface_storage,
|
||||
$code_location,
|
||||
$expected_param_count
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ($storage->template_type_extends) {
|
||||
@ -607,6 +625,22 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
$trait_aliases
|
||||
);
|
||||
|
||||
if (isset($storage->template_type_uses_count[strtolower($fq_trait_name)])) {
|
||||
$trait_storage = $codebase->classlike_storage_provider->get($fq_trait_name);
|
||||
$expected_param_count = $storage->template_type_uses_count[strtolower($fq_trait_name)];
|
||||
|
||||
$this->checkTemplateParams(
|
||||
$codebase,
|
||||
$storage,
|
||||
$trait_storage,
|
||||
new CodeLocation(
|
||||
$this,
|
||||
$trait
|
||||
),
|
||||
$expected_param_count
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($trait_node->stmts as $trait_stmt) {
|
||||
if ($trait_stmt instanceof PhpParser\Node\Stmt\Property) {
|
||||
$this->checkForMissingPropertyType($trait_analyzer, $trait_stmt);
|
||||
@ -1327,57 +1361,56 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
Codebase $codebase,
|
||||
ClassLikeStorage $storage,
|
||||
ClassLikeStorage $parent_storage,
|
||||
CodeLocation $code_location
|
||||
CodeLocation $code_location,
|
||||
int $expected_param_count
|
||||
) {
|
||||
$template_type_count = $parent_storage->template_types === null
|
||||
? 0
|
||||
: count($parent_storage->template_types);
|
||||
|
||||
if ($storage->template_type_extends_count !== null) {
|
||||
if ($template_type_count > $storage->template_type_extends_count) {
|
||||
if (IssueBuffer::accepts(
|
||||
new MissingTemplateParam(
|
||||
$storage->name . ' has missing template params, expecting '
|
||||
. $template_type_count,
|
||||
$code_location
|
||||
),
|
||||
array_merge($storage->suppressed_issues, $this->getSuppressedIssues())
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} elseif ($template_type_count < $storage->template_type_extends_count) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TooManyTemplateParams(
|
||||
$storage->name . ' has too many template params, expecting '
|
||||
. $template_type_count,
|
||||
$code_location
|
||||
),
|
||||
array_merge($storage->suppressed_issues, $this->getSuppressedIssues())
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
if ($template_type_count > $expected_param_count) {
|
||||
if (IssueBuffer::accepts(
|
||||
new MissingTemplateParam(
|
||||
$storage->name . ' has missing template params, expecting '
|
||||
. $template_type_count,
|
||||
$code_location
|
||||
),
|
||||
array_merge($storage->suppressed_issues, $this->getSuppressedIssues())
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
} elseif ($template_type_count < $expected_param_count) {
|
||||
if (IssueBuffer::accepts(
|
||||
new TooManyTemplateParams(
|
||||
$storage->name . ' has too many template params, expecting '
|
||||
. $template_type_count,
|
||||
$code_location
|
||||
),
|
||||
array_merge($storage->suppressed_issues, $this->getSuppressedIssues())
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
if ($parent_storage->template_types && $storage->template_type_extends) {
|
||||
foreach ($parent_storage->template_types as $i => $template_type) {
|
||||
if (!$template_type[0]->isMixed()
|
||||
&& isset($storage->template_type_extends[strtolower($parent_storage->name)][$i])
|
||||
) {
|
||||
$extended_type = new Type\Union([
|
||||
$storage->template_type_extends[strtolower($parent_storage->name)][$i]
|
||||
]);
|
||||
if ($parent_storage->template_types && $storage->template_type_extends) {
|
||||
foreach ($parent_storage->template_types as $i => $template_type) {
|
||||
if (!$template_type[0]->isMixed()
|
||||
&& isset($storage->template_type_extends[strtolower($parent_storage->name)][$i])
|
||||
) {
|
||||
$extended_type = new Type\Union([
|
||||
$storage->template_type_extends[strtolower($parent_storage->name)][$i]
|
||||
]);
|
||||
|
||||
if (!TypeAnalyzer::isContainedBy($codebase, $extended_type, $template_type[0])) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidTemplateParam(
|
||||
'Extended template param ' . $i . ' expects type ' . $template_type[0]->getId()
|
||||
. ', type ' . $extended_type->getId() . ' given',
|
||||
$code_location
|
||||
),
|
||||
array_merge($storage->suppressed_issues, $this->getSuppressedIssues())
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
if (!TypeAnalyzer::isContainedBy($codebase, $extended_type, $template_type[0])) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidTemplateParam(
|
||||
'Extended template param ' . $i . ' expects type ' . $template_type[0]->getId()
|
||||
. ', type ' . $extended_type->getId() . ' given',
|
||||
$code_location
|
||||
),
|
||||
array_merge($storage->suppressed_issues, $this->getSuppressedIssues())
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -582,6 +582,34 @@ class MethodAnalyzer extends FunctionLikeAnalyzer
|
||||
);
|
||||
}
|
||||
|
||||
$guide_trait_name_lc = null;
|
||||
|
||||
if ($guide_classlike_storage === $implementer_classlike_storage) {
|
||||
$guide_trait_name_lc = strtolower($implementer_method_storage->defining_fqcln);
|
||||
}
|
||||
|
||||
if ($guide_trait_name_lc
|
||||
&& isset($implementer_classlike_storage->template_type_extends[$guide_trait_name_lc])
|
||||
) {
|
||||
$map = $implementer_classlike_storage->template_type_extends[$guide_trait_name_lc];
|
||||
|
||||
$template_types = [];
|
||||
|
||||
foreach ($map as $key => $atomic_type) {
|
||||
if (is_string($key)) {
|
||||
$template_types[$key] = [
|
||||
new Type\Union([$atomic_type]),
|
||||
$implementer_method_storage->defining_fqcln
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$implementer_method_storage_return_type->replaceTemplateTypesWithArgTypes(
|
||||
$template_types,
|
||||
$codebase
|
||||
);
|
||||
}
|
||||
|
||||
// treat void as null when comparing against docblock implementer
|
||||
if ($implementer_method_storage_return_type->isVoid()) {
|
||||
$implementer_method_storage_return_type = Type::getNull();
|
||||
|
@ -301,6 +301,53 @@ class Populator
|
||||
|
||||
$this->inheritMethodsFromParent($storage, $trait_storage);
|
||||
$this->inheritPropertiesFromParent($storage, $trait_storage);
|
||||
|
||||
if ($trait_storage->template_types) {
|
||||
if (isset($storage->template_type_extends[$used_trait_lc])) {
|
||||
foreach ($storage->template_type_extends[$used_trait_lc] as $i => $type) {
|
||||
$trait_template_type_names = array_keys($trait_storage->template_types);
|
||||
|
||||
$mapped_name = $trait_template_type_names[$i] ?? null;
|
||||
|
||||
if ($mapped_name) {
|
||||
$storage->template_type_extends[$used_trait_lc][$mapped_name] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($trait_storage->template_type_extends) {
|
||||
foreach ($trait_storage->template_type_extends as $t_storage_class => $type_map) {
|
||||
foreach ($type_map as $i => $type) {
|
||||
if (isset($storage->template_type_extends[$t_storage_class][$i])
|
||||
|| is_int($i)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($type instanceof Type\Atomic\TGenericParam
|
||||
&& $type->defining_class
|
||||
&& ($referenced_type
|
||||
= $storage->template_type_extends
|
||||
[strtolower($type->defining_class)]
|
||||
[$type->param_name]
|
||||
?? null)
|
||||
&& (!$referenced_type instanceof Type\Atomic\TGenericParam)
|
||||
) {
|
||||
$storage->template_type_extends[$t_storage_class][$i] = $referenced_type;
|
||||
} else {
|
||||
$storage->template_type_extends[$t_storage_class][$i] = $type;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$storage->template_type_extends[$used_trait_lc] = [];
|
||||
|
||||
foreach ($trait_storage->template_types as $template_name => $template_type) {
|
||||
$storage->template_type_extends[$used_trait_lc][$template_name]
|
||||
= array_values($template_type[0]->getTypes())[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -226,6 +226,7 @@ class Reflection
|
||||
$storage = $class_storage->methods[strtolower($method_name)] = new MethodStorage();
|
||||
|
||||
$storage->cased_name = $method->name;
|
||||
$storage->defining_fqcln = $method->class;
|
||||
|
||||
if (strtolower((string)$method->name) === strtolower((string)$method->class)) {
|
||||
$this->codebase->methods->setDeclaringMethodId(
|
||||
|
@ -443,6 +443,23 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
$storage->used_traits[strtolower($trait_fqcln)] = $trait_fqcln;
|
||||
$this->file_storage->required_classes[strtolower($trait_fqcln)] = $trait_fqcln;
|
||||
}
|
||||
|
||||
if ($node_comment = $node->getDocComment()) {
|
||||
$comments = DocComment::parse((string) $node_comment, 0);
|
||||
|
||||
if (isset($comments['specials']['template-use'])
|
||||
|| isset($comments['specials']['use'])
|
||||
) {
|
||||
$all_inheritance = array_merge(
|
||||
$comments['specials']['template-use'] ?? [],
|
||||
$comments['specials']['use'] ?? []
|
||||
);
|
||||
|
||||
foreach ($all_inheritance as $template_line) {
|
||||
$this->useTemplatedType($storage, $node, $template_line);
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($node instanceof PhpParser\Node\Expr\Include_) {
|
||||
$this->visitInclude($node);
|
||||
} elseif ($node instanceof PhpParser\Node\Expr\Assign
|
||||
@ -1097,7 +1114,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
|
||||
$implemented_type_parameters = [];
|
||||
|
||||
$storage->template_type_implements_count = count($atomic_type->type_params);
|
||||
$storage->template_type_implements_count[$generic_class_lc] = count($atomic_type->type_params);
|
||||
|
||||
foreach ($atomic_type->type_params as $type_param) {
|
||||
if (!$type_param->isSingle()) {
|
||||
@ -1122,6 +1139,101 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function useTemplatedType(
|
||||
ClassLikeStorage $storage,
|
||||
PhpParser\Node\Stmt\TraitUse $node,
|
||||
string $used_class_name
|
||||
) {
|
||||
try {
|
||||
$used_union_type = Type::parseTokens(
|
||||
Type::fixUpLocalType(
|
||||
$used_class_name,
|
||||
$this->aliases,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases
|
||||
),
|
||||
false,
|
||||
$this->class_template_types
|
||||
);
|
||||
} 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;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$used_union_type->isSingle()) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'@template-use cannot be a union type',
|
||||
new CodeLocation($this->file_scanner, $node, null, true)
|
||||
)
|
||||
)) {
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($used_union_type->getTypes() as $atomic_type) {
|
||||
if (!$atomic_type instanceof Type\Atomic\TGenericObject) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'@template-use has invalid class ' . $atomic_type->getId(),
|
||||
new CodeLocation($this->file_scanner, $node, null, true)
|
||||
)
|
||||
)) {
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$generic_class_lc = strtolower($atomic_type->value);
|
||||
|
||||
if (!isset($storage->used_traits[$generic_class_lc])) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'@template-use must include the name of an used class,'
|
||||
. ' got ' . $atomic_type->getId(),
|
||||
new CodeLocation($this->file_scanner, $node, null, true)
|
||||
)
|
||||
)) {
|
||||
}
|
||||
}
|
||||
|
||||
$used_type_parameters = [];
|
||||
|
||||
$storage->template_type_uses_count[$generic_class_lc] = count($atomic_type->type_params);
|
||||
|
||||
foreach ($atomic_type->type_params as $type_param) {
|
||||
if (!$type_param->isSingle()) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'@template-uses type parameter cannot be a union type',
|
||||
new CodeLocation($this->file_scanner, $node, null, true)
|
||||
)
|
||||
)) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($type_param->getTypes() as $type_param_atomic) {
|
||||
$used_type_parameters[] = $type_param_atomic;
|
||||
}
|
||||
}
|
||||
|
||||
if ($used_type_parameters) {
|
||||
$storage->template_type_extends[$generic_class_lc] = $used_type_parameters;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node\FunctionLike $stmt
|
||||
* @param bool $fake_method in the case of @method annotations we do something a little strange
|
||||
@ -1137,6 +1249,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
$cased_function_id = '@method ' . $stmt->name->name;
|
||||
|
||||
$storage = new MethodStorage();
|
||||
$storage->defining_fqcln = '';
|
||||
$storage->is_static = (bool) $stmt->isStatic();
|
||||
} elseif ($stmt instanceof PhpParser\Node\Stmt\Function_) {
|
||||
$cased_function_id =
|
||||
@ -1251,6 +1364,8 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
$storage = $class_storage->methods[strtolower($stmt->name->name)] = new MethodStorage();
|
||||
}
|
||||
|
||||
$storage->defining_fqcln = $fq_classlike_name;
|
||||
|
||||
$class_name_parts = explode('\\', $fq_classlike_name);
|
||||
$class_name = array_pop($class_name_parts);
|
||||
|
||||
|
@ -281,15 +281,15 @@ class ClassLikeStorage
|
||||
public $template_type_extends_count;
|
||||
|
||||
/**
|
||||
* @var array<string, array<int|string, Type\Atomic>>|null
|
||||
*/
|
||||
public $template_type_implements;
|
||||
|
||||
/**
|
||||
* @var ?int
|
||||
* @var array<string, int>|null
|
||||
*/
|
||||
public $template_type_implements_count;
|
||||
|
||||
/**
|
||||
* @var array<string, int>|null
|
||||
*/
|
||||
public $template_type_uses_count;
|
||||
|
||||
/**
|
||||
* @var array<string, array<int, CodeLocation>>|null
|
||||
*/
|
||||
|
@ -49,4 +49,9 @@ class MethodStorage extends FunctionLikeStorage
|
||||
* @var bool
|
||||
*/
|
||||
public $inheritdoc = false;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $defining_fqcln;
|
||||
}
|
||||
|
@ -844,6 +844,43 @@ class TemplateExtendsTest extends TestCase
|
||||
public function valid(): bool { return false; }
|
||||
}',
|
||||
],
|
||||
'traitUse' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
trait CollectionTrait
|
||||
{
|
||||
/**
|
||||
* @return array<T>
|
||||
*/
|
||||
abstract function elements() : array;
|
||||
|
||||
/**
|
||||
* @return T|null
|
||||
*/
|
||||
public function first()
|
||||
{
|
||||
return $this->elements()[0] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
class Service
|
||||
{
|
||||
/**
|
||||
* @use CollectionTrait<int>
|
||||
*/
|
||||
use CollectionTrait;
|
||||
|
||||
/**
|
||||
* @return array<int>
|
||||
*/
|
||||
public function elements(): array
|
||||
{
|
||||
return [1, 2, 3, 4];
|
||||
}
|
||||
}',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -1126,6 +1163,21 @@ class TemplateExtendsTest extends TestCase
|
||||
class CC extends A {}',
|
||||
'error_message' => 'MissingTemplateParam'
|
||||
],
|
||||
'templateImplementsWithoutAllParams' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template T
|
||||
* @template V
|
||||
* @template U
|
||||
*/
|
||||
interface I {}
|
||||
|
||||
/**
|
||||
* @implements I<int>
|
||||
*/
|
||||
class CC implements I {}',
|
||||
'error_message' => 'MissingTemplateParam'
|
||||
],
|
||||
'extendsTemplateButLikeBadly' => [
|
||||
'<?php
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user