mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
parent
20ab8ee736
commit
fa73c7c9d9
@ -51,6 +51,7 @@ use function str_replace;
|
||||
use function count;
|
||||
use function array_search;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -486,8 +487,11 @@ class ClassAnalyzer extends ClassLikeAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
if ($storage->mixin && $storage->mixin_declaring_fqcln === $storage->name) {
|
||||
$union = new Type\Union([$storage->mixin]);
|
||||
if (($storage->templatedMixins || $storage->namedMixins)
|
||||
&& $storage->mixin_declaring_fqcln === $storage->name) {
|
||||
/** @var non-empty-array<int, Type\Atomic\TTemplateParam|Type\Atomic\TNamedObject> $mixins */
|
||||
$mixins = array_merge($storage->templatedMixins, $storage->namedMixins);
|
||||
$union = new Type\Union($mixins);
|
||||
$union->check(
|
||||
$this,
|
||||
new CodeLocation(
|
||||
|
@ -924,14 +924,22 @@ class CommentAnalyzer
|
||||
}
|
||||
|
||||
if (isset($parsed_docblock->tags['mixin'])) {
|
||||
$mixin = trim(reset($parsed_docblock->tags['mixin']));
|
||||
$doc_line_parts = self::splitDocLine($mixin);
|
||||
$mixin = $doc_line_parts[0];
|
||||
foreach ($parsed_docblock->tags['mixin'] as $rawMixin) {
|
||||
$mixin = trim($rawMixin);
|
||||
$doc_line_parts = self::splitDocLine($mixin);
|
||||
$mixin = $doc_line_parts[0];
|
||||
|
||||
if ($mixin) {
|
||||
$info->mixin = $mixin;
|
||||
} else {
|
||||
throw new DocblockParseException('@mixin annotation used without specifying class');
|
||||
if ($mixin) {
|
||||
$info->mixins[] = $mixin;
|
||||
} else {
|
||||
throw new DocblockParseException('@mixin annotation used without specifying class');
|
||||
}
|
||||
}
|
||||
|
||||
// backwards compatibility
|
||||
if ($info->mixins) {
|
||||
/** @psalm-suppress DeprecatedProperty */
|
||||
$info->mixin = reset($info->mixins);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -258,120 +258,128 @@ class AtomicMethodCallAnalyzer extends CallAnalyzer
|
||||
);
|
||||
|
||||
if (!$naive_method_exists
|
||||
&& $class_storage->mixin instanceof Type\Atomic\TTemplateParam
|
||||
&& $class_storage->templatedMixins
|
||||
&& $lhs_type_part instanceof Type\Atomic\TGenericObject
|
||||
&& $class_storage->template_types
|
||||
) {
|
||||
$param_position = \array_search(
|
||||
$class_storage->mixin->param_name,
|
||||
\array_keys($class_storage->template_types)
|
||||
);
|
||||
foreach ($class_storage->templatedMixins as $mixin) {
|
||||
$param_position = \array_search(
|
||||
$mixin->param_name,
|
||||
\array_keys($class_storage->template_types)
|
||||
);
|
||||
|
||||
if ($param_position !== false
|
||||
&& isset($lhs_type_part->type_params[$param_position])
|
||||
) {
|
||||
if ($lhs_type_part->type_params[$param_position]->isSingle()) {
|
||||
$lhs_type_part_new = array_values(
|
||||
$lhs_type_part->type_params[$param_position]->getAtomicTypes()
|
||||
)[0];
|
||||
if ($param_position !== false
|
||||
&& isset($lhs_type_part->type_params[$param_position])
|
||||
) {
|
||||
/** @var Type\Union $current_type_param */
|
||||
$current_type_param = $lhs_type_part->type_params[$param_position];
|
||||
if ($current_type_param->isSingle()) {
|
||||
$lhs_type_part_new = array_values(
|
||||
$current_type_param->getAtomicTypes()
|
||||
)[0];
|
||||
|
||||
if ($lhs_type_part_new instanceof Type\Atomic\TNamedObject) {
|
||||
$new_method_id = new MethodIdentifier(
|
||||
$lhs_type_part_new->value,
|
||||
$method_name_lc
|
||||
);
|
||||
if ($lhs_type_part_new instanceof Type\Atomic\TNamedObject) {
|
||||
$new_method_id = new MethodIdentifier(
|
||||
$lhs_type_part_new->value,
|
||||
$method_name_lc
|
||||
);
|
||||
|
||||
$mixin_class_storage = $codebase->classlike_storage_provider->get($lhs_type_part_new->value);
|
||||
$mixin_class_storage = $codebase->classlike_storage_provider->get(
|
||||
$lhs_type_part_new->value
|
||||
);
|
||||
|
||||
if ($codebase->methods->methodExists(
|
||||
$new_method_id,
|
||||
$context->calling_method_id,
|
||||
$codebase->collect_locations
|
||||
? new CodeLocation($source, $stmt->name)
|
||||
: null,
|
||||
!$context->collect_initializations
|
||||
if ($codebase->methods->methodExists(
|
||||
$new_method_id,
|
||||
$context->calling_method_id,
|
||||
$codebase->collect_locations
|
||||
? new CodeLocation($source, $stmt->name)
|
||||
: null,
|
||||
!$context->collect_initializations
|
||||
&& !$context->collect_mutations
|
||||
? $statements_analyzer
|
||||
: null,
|
||||
$statements_analyzer->getFilePath()
|
||||
)) {
|
||||
$lhs_type_part = clone $lhs_type_part_new;
|
||||
$class_storage = $mixin_class_storage;
|
||||
? $statements_analyzer
|
||||
: null,
|
||||
$statements_analyzer->getFilePath()
|
||||
)) {
|
||||
$lhs_type_part = clone $lhs_type_part_new;
|
||||
$class_storage = $mixin_class_storage;
|
||||
|
||||
$naive_method_exists = true;
|
||||
$method_id = $new_method_id;
|
||||
} elseif (isset($mixin_class_storage->pseudo_methods[$method_name_lc])) {
|
||||
$lhs_type_part = clone $lhs_type_part_new;
|
||||
$class_storage = $mixin_class_storage;
|
||||
$method_id = $new_method_id;
|
||||
$naive_method_exists = true;
|
||||
$method_id = $new_method_id;
|
||||
} elseif (isset($mixin_class_storage->pseudo_methods[$method_name_lc])) {
|
||||
$lhs_type_part = clone $lhs_type_part_new;
|
||||
$class_storage = $mixin_class_storage;
|
||||
$method_id = $new_method_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif (!$naive_method_exists
|
||||
&& $class_storage->mixin_declaring_fqcln
|
||||
&& $class_storage->mixin instanceof Type\Atomic\TNamedObject
|
||||
&& $class_storage->namedMixins
|
||||
) {
|
||||
$new_method_id = new MethodIdentifier(
|
||||
$class_storage->mixin->value,
|
||||
$method_name_lc
|
||||
);
|
||||
foreach ($class_storage->namedMixins as $mixin) {
|
||||
$new_method_id = new MethodIdentifier(
|
||||
$mixin->value,
|
||||
$method_name_lc
|
||||
);
|
||||
|
||||
if ($codebase->methods->methodExists(
|
||||
$new_method_id,
|
||||
$context->calling_method_id,
|
||||
$codebase->collect_locations
|
||||
? new CodeLocation($source, $stmt->name)
|
||||
: null,
|
||||
!$context->collect_initializations
|
||||
if ($codebase->methods->methodExists(
|
||||
$new_method_id,
|
||||
$context->calling_method_id,
|
||||
$codebase->collect_locations
|
||||
? new CodeLocation($source, $stmt->name)
|
||||
: null,
|
||||
!$context->collect_initializations
|
||||
&& !$context->collect_mutations
|
||||
? $statements_analyzer
|
||||
: null,
|
||||
$statements_analyzer->getFilePath()
|
||||
)) {
|
||||
$mixin_declaring_class_storage = $codebase->classlike_storage_provider->get(
|
||||
$class_storage->mixin_declaring_fqcln
|
||||
);
|
||||
? $statements_analyzer
|
||||
: null,
|
||||
$statements_analyzer->getFilePath()
|
||||
)) {
|
||||
$mixin_declaring_class_storage = $codebase->classlike_storage_provider->get(
|
||||
$class_storage->mixin_declaring_fqcln
|
||||
);
|
||||
|
||||
$mixin_class_template_params = ClassTemplateParamCollector::collect(
|
||||
$codebase,
|
||||
$mixin_declaring_class_storage,
|
||||
$codebase->classlike_storage_provider->get($fq_class_name),
|
||||
null,
|
||||
$lhs_type_part,
|
||||
$lhs_var_id
|
||||
);
|
||||
$mixin_class_template_params = ClassTemplateParamCollector::collect(
|
||||
$codebase,
|
||||
$mixin_declaring_class_storage,
|
||||
$codebase->classlike_storage_provider->get($fq_class_name),
|
||||
null,
|
||||
$lhs_type_part,
|
||||
$lhs_var_id
|
||||
);
|
||||
|
||||
$lhs_type_part = clone $class_storage->mixin;
|
||||
$lhs_type_part = clone $mixin;
|
||||
|
||||
$lhs_type_part->replaceTemplateTypesWithArgTypes(
|
||||
new \Psalm\Internal\Type\TemplateResult([], $mixin_class_template_params ?: []),
|
||||
$codebase
|
||||
);
|
||||
$lhs_type_part->replaceTemplateTypesWithArgTypes(
|
||||
new \Psalm\Internal\Type\TemplateResult([], $mixin_class_template_params ?: []),
|
||||
$codebase
|
||||
);
|
||||
|
||||
$lhs_type_expanded = \Psalm\Internal\Type\TypeExpander::expandUnion(
|
||||
$codebase,
|
||||
new Type\Union([$lhs_type_part]),
|
||||
$mixin_declaring_class_storage->name,
|
||||
$fq_class_name,
|
||||
$class_storage->parent_class,
|
||||
true,
|
||||
false,
|
||||
$class_storage->final
|
||||
);
|
||||
$lhs_type_expanded = \Psalm\Internal\Type\TypeExpander::expandUnion(
|
||||
$codebase,
|
||||
new Type\Union([$lhs_type_part]),
|
||||
$mixin_declaring_class_storage->name,
|
||||
$fq_class_name,
|
||||
$class_storage->parent_class,
|
||||
true,
|
||||
false,
|
||||
$class_storage->final
|
||||
);
|
||||
|
||||
$new_lhs_type_part = array_values($lhs_type_expanded->getAtomicTypes())[0];
|
||||
$new_lhs_type_part = array_values($lhs_type_expanded->getAtomicTypes())[0];
|
||||
|
||||
if ($new_lhs_type_part instanceof Type\Atomic\TNamedObject) {
|
||||
$lhs_type_part = $new_lhs_type_part;
|
||||
if ($new_lhs_type_part instanceof Type\Atomic\TNamedObject) {
|
||||
$lhs_type_part = $new_lhs_type_part;
|
||||
}
|
||||
|
||||
$mixin_class_storage = $codebase->classlike_storage_provider->get($mixin->value);
|
||||
|
||||
$fq_class_name = $mixin_class_storage->name;
|
||||
$class_storage = $mixin_class_storage;
|
||||
$naive_method_exists = true;
|
||||
$method_id = $new_method_id;
|
||||
}
|
||||
|
||||
$mixin_class_storage = $codebase->classlike_storage_provider->get($class_storage->mixin->value);
|
||||
|
||||
$fq_class_name = $mixin_class_storage->name;
|
||||
$class_storage = $mixin_class_storage;
|
||||
$naive_method_exists = true;
|
||||
$method_id = $new_method_id;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -37,6 +37,7 @@ use function strlen;
|
||||
use function substr;
|
||||
use Psalm\Internal\Taint\Source;
|
||||
use Psalm\Internal\Taint\TaintNode;
|
||||
use function array_filter;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
@ -449,83 +450,112 @@ class StaticCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
|
||||
|
||||
if (!$naive_method_exists
|
||||
&& $class_storage->mixin_declaring_fqcln
|
||||
&& $class_storage->mixin instanceof Type\Atomic\TNamedObject
|
||||
&& $class_storage->namedMixins
|
||||
) {
|
||||
$new_method_id = new MethodIdentifier(
|
||||
$class_storage->mixin->value,
|
||||
$method_name_lc
|
||||
);
|
||||
foreach ($class_storage->namedMixins as $mixin) {
|
||||
$new_method_id = new MethodIdentifier(
|
||||
$mixin->value,
|
||||
$method_name_lc
|
||||
);
|
||||
|
||||
if ($codebase->methods->methodExists(
|
||||
$new_method_id,
|
||||
$context->calling_method_id,
|
||||
$codebase->collect_locations
|
||||
? new CodeLocation($source, $stmt->name)
|
||||
: null,
|
||||
!$context->collect_initializations
|
||||
if ($codebase->methods->methodExists(
|
||||
$new_method_id,
|
||||
$context->calling_method_id,
|
||||
$codebase->collect_locations
|
||||
? new CodeLocation($source, $stmt->name)
|
||||
: null,
|
||||
!$context->collect_initializations
|
||||
&& !$context->collect_mutations
|
||||
? $statements_analyzer
|
||||
: null,
|
||||
$statements_analyzer->getFilePath()
|
||||
)) {
|
||||
$mixin_candidate_type = new Type\Union([clone $class_storage->mixin]);
|
||||
? $statements_analyzer
|
||||
: null,
|
||||
$statements_analyzer->getFilePath()
|
||||
)) {
|
||||
$mixin_candidates = [];
|
||||
foreach ($class_storage->templatedMixins as $mixin_candidate) {
|
||||
$mixin_candidates[] = clone $mixin_candidate;
|
||||
}
|
||||
|
||||
if ($class_storage->mixin instanceof Type\Atomic\TGenericObject) {
|
||||
$mixin_declaring_class_storage = $codebase->classlike_storage_provider->get(
|
||||
$class_storage->mixin_declaring_fqcln
|
||||
);
|
||||
foreach ($class_storage->namedMixins as $mixin_candidate) {
|
||||
$mixin_candidates[] = clone $mixin_candidate;
|
||||
}
|
||||
|
||||
$mixin_candidate_type = InstancePropertyFetchAnalyzer::localizePropertyType(
|
||||
$mixin_candidates_no_generic = array_filter($mixin_candidates, function ($check) {
|
||||
return !($check instanceof Type\Atomic\TGenericObject);
|
||||
});
|
||||
|
||||
// $mixin_candidates_no_generic will only be empty when there are TGenericObject entries.
|
||||
// In that case, Union will be initialized with an empty array but
|
||||
// replaced with non-empty types in the following loop.
|
||||
/** @psalm-suppress ArgumentTypeCoercion */
|
||||
$mixin_candidate_type = new Type\Union($mixin_candidates_no_generic);
|
||||
|
||||
foreach ($mixin_candidates as $tGenericMixin) {
|
||||
if (!($tGenericMixin instanceof Type\Atomic\TGenericObject)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$mixin_declaring_class_storage = $codebase->classlike_storage_provider->get(
|
||||
$class_storage->mixin_declaring_fqcln
|
||||
);
|
||||
|
||||
$new_mixin_candidate_type = InstancePropertyFetchAnalyzer::localizePropertyType(
|
||||
$codebase,
|
||||
new Type\Union([$lhs_type_part]),
|
||||
$tGenericMixin,
|
||||
$class_storage,
|
||||
$mixin_declaring_class_storage
|
||||
);
|
||||
|
||||
foreach ($mixin_candidate_type->getAtomicTypes() as $type) {
|
||||
$new_mixin_candidate_type->addType($type);
|
||||
}
|
||||
|
||||
$mixin_candidate_type = $new_mixin_candidate_type;
|
||||
}
|
||||
|
||||
$new_lhs_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
|
||||
$codebase,
|
||||
new Type\Union([$lhs_type_part]),
|
||||
$class_storage->mixin,
|
||||
$class_storage,
|
||||
$mixin_declaring_class_storage
|
||||
$mixin_candidate_type,
|
||||
$fq_class_name,
|
||||
$fq_class_name,
|
||||
$class_storage->parent_class,
|
||||
true,
|
||||
false,
|
||||
$class_storage->final
|
||||
);
|
||||
|
||||
$old_data_provider = $statements_analyzer->node_data;
|
||||
|
||||
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
||||
|
||||
$context->vars_in_scope['$tmp_mixin_var'] = $new_lhs_type;
|
||||
|
||||
$fake_method_call_expr = new PhpParser\Node\Expr\MethodCall(
|
||||
new PhpParser\Node\Expr\Variable(
|
||||
'tmp_mixin_var',
|
||||
$stmt->class->getAttributes()
|
||||
),
|
||||
$stmt->name,
|
||||
$stmt->args,
|
||||
$stmt->getAttributes()
|
||||
);
|
||||
|
||||
if (MethodCallAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$fake_method_call_expr,
|
||||
$context
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fake_method_call_type = $statements_analyzer->node_data->getType($fake_method_call_expr);
|
||||
|
||||
$statements_analyzer->node_data = $old_data_provider;
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, $fake_method_call_type ?: Type::getMixed());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$new_lhs_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
|
||||
$codebase,
|
||||
$mixin_candidate_type,
|
||||
$fq_class_name,
|
||||
$fq_class_name,
|
||||
$class_storage->parent_class,
|
||||
true,
|
||||
false,
|
||||
$class_storage->final
|
||||
);
|
||||
|
||||
$old_data_provider = $statements_analyzer->node_data;
|
||||
|
||||
$statements_analyzer->node_data = clone $statements_analyzer->node_data;
|
||||
|
||||
$context->vars_in_scope['$tmp_mixin_var'] = $new_lhs_type;
|
||||
|
||||
$fake_method_call_expr = new PhpParser\Node\Expr\MethodCall(
|
||||
new PhpParser\Node\Expr\Variable(
|
||||
'tmp_mixin_var',
|
||||
$stmt->class->getAttributes()
|
||||
),
|
||||
$stmt->name,
|
||||
$stmt->args,
|
||||
$stmt->getAttributes()
|
||||
);
|
||||
|
||||
if (MethodCallAnalyzer::analyze(
|
||||
$statements_analyzer,
|
||||
$fake_method_call_expr,
|
||||
$context
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$fake_method_call_type = $statements_analyzer->node_data->getType($fake_method_call_expr);
|
||||
|
||||
$statements_analyzer->node_data = $old_data_provider;
|
||||
|
||||
$statements_analyzer->node_data->setType($stmt, $fake_method_call_type ?: Type::getMixed());
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -501,37 +501,39 @@ class InstancePropertyFetchAnalyzer
|
||||
$get_method_id = new \Psalm\Internal\MethodIdentifier($fq_class_name, '__get');
|
||||
|
||||
if (!$naive_property_exists
|
||||
&& $class_storage->mixin instanceof Type\Atomic\TNamedObject
|
||||
&& $class_storage->namedMixins
|
||||
) {
|
||||
$new_property_id = $class_storage->mixin->value . '::$' . $prop_name;
|
||||
foreach ($class_storage->namedMixins as $mixin) {
|
||||
$new_property_id = $mixin->value . '::$' . $prop_name;
|
||||
|
||||
try {
|
||||
$new_class_storage = $codebase->classlike_storage_provider->get($class_storage->mixin->value);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$new_class_storage = null;
|
||||
}
|
||||
|
||||
if ($new_class_storage
|
||||
&& ($codebase->properties->propertyExists(
|
||||
$new_property_id,
|
||||
true,
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
$codebase->collect_locations
|
||||
? new CodeLocation($statements_analyzer->getSource(), $stmt)
|
||||
: null
|
||||
)
|
||||
|| isset($new_class_storage->pseudo_property_get_types['$' . $prop_name]))
|
||||
) {
|
||||
$fq_class_name = $class_storage->mixin->value;
|
||||
$lhs_type_part = clone $class_storage->mixin;
|
||||
$class_storage = $new_class_storage;
|
||||
|
||||
if (!isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) {
|
||||
$naive_property_exists = true;
|
||||
try {
|
||||
$new_class_storage = $codebase->classlike_storage_provider->get($mixin->value);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$new_class_storage = null;
|
||||
}
|
||||
|
||||
$property_id = $new_property_id;
|
||||
if ($new_class_storage
|
||||
&& ($codebase->properties->propertyExists(
|
||||
$new_property_id,
|
||||
true,
|
||||
$statements_analyzer,
|
||||
$context,
|
||||
$codebase->collect_locations
|
||||
? new CodeLocation($statements_analyzer->getSource(), $stmt)
|
||||
: null
|
||||
)
|
||||
|| isset($new_class_storage->pseudo_property_get_types['$' . $prop_name]))
|
||||
) {
|
||||
$fq_class_name = $mixin->value;
|
||||
$lhs_type_part = clone $mixin;
|
||||
$class_storage = $new_class_storage;
|
||||
|
||||
if (!isset($new_class_storage->pseudo_property_get_types['$' . $prop_name])) {
|
||||
$naive_property_exists = true;
|
||||
}
|
||||
|
||||
$property_id = $new_property_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -594,9 +594,17 @@ class Populator
|
||||
$storage->protected_class_constants
|
||||
);
|
||||
|
||||
if ($parent_storage->mixin && !$storage->mixin) {
|
||||
if (($parent_storage->namedMixins || $parent_storage->templatedMixins)
|
||||
&& (!$storage->namedMixins || !$storage->templatedMixins)) {
|
||||
$storage->mixin_declaring_fqcln = $parent_storage->mixin_declaring_fqcln;
|
||||
$storage->mixin = $parent_storage->mixin;
|
||||
|
||||
if (!$storage->namedMixins) {
|
||||
$storage->namedMixins = $parent_storage->namedMixins;
|
||||
}
|
||||
|
||||
if (!$storage->templatedMixins) {
|
||||
$storage->templatedMixins = $parent_storage->templatedMixins;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($parent_storage->public_class_constant_nodes as $name => $_) {
|
||||
|
@ -1327,10 +1327,10 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
|
||||
$storage->final = $storage->final || $docblock_info->final;
|
||||
|
||||
if ($docblock_info->mixin) {
|
||||
foreach ($docblock_info->mixins as $key => $mixin) {
|
||||
$mixin_type = TypeParser::parseTokens(
|
||||
TypeTokenizer::getFullyQualifiedTokens(
|
||||
$docblock_info->mixin,
|
||||
$mixin,
|
||||
$this->aliases,
|
||||
$this->class_template_types,
|
||||
$this->type_aliases,
|
||||
@ -1352,11 +1352,23 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
if ($mixin_type->isSingle()) {
|
||||
$mixin_type = \array_values($mixin_type->getAtomicTypes())[0];
|
||||
|
||||
if ($mixin_type instanceof Type\Atomic\TNamedObject) {
|
||||
$storage->namedMixins[] = $mixin_type;
|
||||
}
|
||||
|
||||
if ($mixin_type instanceof Type\Atomic\TTemplateParam) {
|
||||
$storage->templatedMixins[] = $mixin_type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($key === 0) {
|
||||
$storage->mixin_declaring_fqcln = $storage->name;
|
||||
|
||||
// backwards compatibility
|
||||
if ($mixin_type instanceof Type\Atomic\TNamedObject
|
||||
|| $mixin_type instanceof Type\Atomic\TTemplateParam
|
||||
) {
|
||||
|| $mixin_type instanceof Type\Atomic\TTemplateParam) {
|
||||
/** @psalm-suppress DeprecatedProperty **/
|
||||
$storage->mixin = $mixin_type;
|
||||
$storage->mixin_declaring_fqcln = $storage->name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -36,9 +36,15 @@ class ClassLikeDocblockComment
|
||||
|
||||
/**
|
||||
* @var null|string
|
||||
* @deprecated
|
||||
*/
|
||||
public $mixin = null;
|
||||
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
public $mixins = [];
|
||||
|
||||
/**
|
||||
* @var array<int, array{string, ?string, ?string, bool, int}>
|
||||
*/
|
||||
|
@ -94,9 +94,20 @@ class ClassLikeStorage
|
||||
|
||||
/**
|
||||
* @var null|Type\Atomic\TTemplateParam|Type\Atomic\TNamedObject
|
||||
* @deprecated
|
||||
*/
|
||||
public $mixin = null;
|
||||
|
||||
/**
|
||||
* @var Type\Atomic\TTemplateParam[]
|
||||
*/
|
||||
public $templatedMixins = [];
|
||||
|
||||
/**
|
||||
* @var Type\Atomic\TNamedObject[]
|
||||
*/
|
||||
public $namedMixins = [];
|
||||
|
||||
/**
|
||||
* @var ?string
|
||||
*/
|
||||
|
@ -400,6 +400,137 @@ class MixinAnnotationTest extends TestCase
|
||||
return $b->active();
|
||||
}'
|
||||
],
|
||||
'multipleMixins' => [
|
||||
'<?php
|
||||
class MixinA {
|
||||
function a(): string { return "foo"; }
|
||||
}
|
||||
|
||||
class MixinB {
|
||||
function b(): int { return 0; }
|
||||
}
|
||||
|
||||
/**
|
||||
* @mixin MixinA
|
||||
* @mixin MixinB
|
||||
*/
|
||||
class Test {}
|
||||
|
||||
$test = new Test();
|
||||
|
||||
$a = $test->a();
|
||||
$b = $test->b();',
|
||||
'assertions' => [
|
||||
'$a' => 'string',
|
||||
'$b' => 'int',
|
||||
],
|
||||
],
|
||||
'inheritMultipleTemplatedMixinsWithStatic' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
class Mixin {
|
||||
/**
|
||||
* @psalm-var T
|
||||
*/
|
||||
private $var;
|
||||
|
||||
/**
|
||||
* @psalm-param T $var
|
||||
*/
|
||||
public function __construct ($var) {
|
||||
$this->var = $var;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return T
|
||||
*/
|
||||
public function type() {
|
||||
return $this->var;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
class OtherMixin {
|
||||
/**
|
||||
* @psalm-var T
|
||||
*/
|
||||
private $var;
|
||||
|
||||
/**
|
||||
* @psalm-param T $var
|
||||
*/
|
||||
public function __construct ($var) {
|
||||
$this->var = $var;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-return T
|
||||
*/
|
||||
public function other() {
|
||||
return $this->var;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T as object
|
||||
* @template T2 as string
|
||||
* @mixin Mixin<T>
|
||||
* @mixin OtherMixin<T2>
|
||||
*/
|
||||
abstract class Foo {
|
||||
/** @var Mixin<T> */
|
||||
public object $obj;
|
||||
|
||||
/** @var OtherMixin<T2> */
|
||||
public object $otherObj;
|
||||
|
||||
public function __call(string $name, array $args) {
|
||||
if ($name === "test") {
|
||||
return $this->obj->$name(...$args);
|
||||
}
|
||||
|
||||
return $this->otherObj->$name(...$args);
|
||||
}
|
||||
|
||||
public function __callStatic(string $name, array $args) {
|
||||
if ($name === "test") {
|
||||
return (new static)->obj->$name(...$args);
|
||||
}
|
||||
|
||||
return (new static)->otherObj->$name(...$args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @extends Foo<static, string>
|
||||
*/
|
||||
abstract class FooChild extends Foo{}
|
||||
|
||||
/**
|
||||
* @psalm-suppress MissingConstructor
|
||||
*/
|
||||
final class FooGrandChild extends FooChild {}
|
||||
|
||||
function test() : FooGrandChild {
|
||||
return FooGrandChild::type();
|
||||
}
|
||||
|
||||
function testStatic() : FooGrandChild {
|
||||
return (new FooGrandChild)->type();
|
||||
}
|
||||
|
||||
function other() : string {
|
||||
return FooGrandChild::other();
|
||||
}
|
||||
|
||||
function otherStatic() : string {
|
||||
return (new FooGrandChild)->other();
|
||||
}'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user