1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Fix #3757 - allow multiple mixins (#3772)

This commit is contained in:
Daniel Melchior 2020-08-05 21:49:19 +02:00 committed by GitHub
parent 20ab8ee736
commit fa73c7c9d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 421 additions and 201 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 => $_) {

View File

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

View File

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

View File

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

View File

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