mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Add initial support for @mixin Foo
This commit is contained in:
parent
9177b410cb
commit
d5d4a1826d
@ -876,6 +876,17 @@ class CommentAnalyzer
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($parsed_docblock['specials']['mixin'])) {
|
||||
$mixin = trim(reset($parsed_docblock['specials']['mixin']));
|
||||
$mixin = explode(' ', $mixin)[0];
|
||||
|
||||
if ($mixin) {
|
||||
$info->mixin = $mixin;
|
||||
} else {
|
||||
throw new DocblockParseException('@mixin annotation used without specifying class');
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($parsed_docblock['specials']['psalm-seal-properties'])) {
|
||||
$info->sealed_properties = true;
|
||||
}
|
||||
|
@ -177,6 +177,10 @@ class Populator
|
||||
|
||||
$this->populateDataFromTraits($storage, $storage_provider, $dependent_classlikes);
|
||||
|
||||
if ($storage->mixin_fqcln) {
|
||||
$this->populateDataFromMixin($storage, $storage_provider, $dependent_classlikes, $storage->mixin_fqcln);
|
||||
}
|
||||
|
||||
$dependent_classlikes[$fq_classlike_name_lc] = true;
|
||||
|
||||
if ($storage->parent_classes) {
|
||||
@ -420,6 +424,30 @@ class Populator
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
private function populateDataFromMixin(
|
||||
ClassLikeStorage $storage,
|
||||
ClassLikeStorageProvider $storage_provider,
|
||||
array $dependent_classlikes,
|
||||
string $mixin_fqcln
|
||||
) {
|
||||
try {
|
||||
$mixin_fqcln = $this->classlikes->getUnAliasedName(
|
||||
$mixin_fqcln
|
||||
);
|
||||
$mixin_storage = $storage_provider->get($mixin_fqcln);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->populateClassLikeStorage($mixin_storage, $dependent_classlikes);
|
||||
|
||||
$this->inheritMethodsFromParent($storage, $mixin_storage);
|
||||
$this->inheritPropertiesFromParent($storage, $mixin_storage, false);
|
||||
}
|
||||
|
||||
private static function extendType(
|
||||
Type\Union $type,
|
||||
ClassLikeStorage $storage
|
||||
@ -1013,8 +1041,10 @@ class Populator
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function inheritMethodsFromParent(ClassLikeStorage $storage, ClassLikeStorage $parent_storage)
|
||||
{
|
||||
protected function inheritMethodsFromParent(
|
||||
ClassLikeStorage $storage,
|
||||
ClassLikeStorage $parent_storage
|
||||
) {
|
||||
$fq_class_name = $storage->name;
|
||||
|
||||
// register where they appear (can never be in a trait)
|
||||
@ -1119,8 +1149,11 @@ class Populator
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function inheritPropertiesFromParent(ClassLikeStorage $storage, ClassLikeStorage $parent_storage)
|
||||
{
|
||||
private function inheritPropertiesFromParent(
|
||||
ClassLikeStorage $storage,
|
||||
ClassLikeStorage $parent_storage,
|
||||
bool $include_protected = true
|
||||
) {
|
||||
// register where they appear (can never be in a trait)
|
||||
foreach ($parent_storage->appearing_property_ids as $property_name => $appearing_property_id) {
|
||||
if (isset($storage->appearing_property_ids[$property_name])) {
|
||||
@ -1129,7 +1162,10 @@ class Populator
|
||||
|
||||
if (!$parent_storage->is_trait
|
||||
&& isset($parent_storage->properties[$property_name])
|
||||
&& $parent_storage->properties[$property_name]->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
|
||||
&& ($parent_storage->properties[$property_name]->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
|
||||
|| (!$include_protected
|
||||
&& $parent_storage->properties[$property_name]->visibility
|
||||
=== ClassLikeAnalyzer::VISIBILITY_PROTECTED))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@ -1148,7 +1184,10 @@ class Populator
|
||||
|
||||
if (!$parent_storage->is_trait
|
||||
&& isset($parent_storage->properties[$property_name])
|
||||
&& $parent_storage->properties[$property_name]->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
|
||||
&& ($parent_storage->properties[$property_name]->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
|
||||
|| (!$include_protected
|
||||
&& $parent_storage->properties[$property_name]->visibility
|
||||
=== ClassLikeAnalyzer::VISIBILITY_PROTECTED))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
@ -1160,7 +1199,10 @@ class Populator
|
||||
foreach ($parent_storage->inheritable_property_ids as $property_name => $inheritable_property_id) {
|
||||
if (!$parent_storage->is_trait
|
||||
&& isset($parent_storage->properties[$property_name])
|
||||
&& $parent_storage->properties[$property_name]->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
|
||||
&& ($parent_storage->properties[$property_name]->visibility === ClassLikeAnalyzer::VISIBILITY_PRIVATE
|
||||
|| (!$include_protected
|
||||
&& $parent_storage->properties[$property_name]->visibility
|
||||
=== ClassLikeAnalyzer::VISIBILITY_PROTECTED))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
@ -27,6 +27,11 @@ class ClassLikeDocblockComment
|
||||
*/
|
||||
public $psalm_internal = null;
|
||||
|
||||
/**
|
||||
* @var null|string
|
||||
*/
|
||||
public $mixin = null;
|
||||
|
||||
/**
|
||||
* @var array<int, array{string, ?string, ?string, bool, int}>
|
||||
*/
|
||||
|
@ -1239,6 +1239,23 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
$storage->internal = $docblock_info->internal;
|
||||
$storage->psalm_internal = $docblock_info->psalm_internal;
|
||||
|
||||
if ($docblock_info->mixin) {
|
||||
if (isset($this->class_template_types[$docblock_info->mixin])) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidDocblock(
|
||||
'Templates are not currently supported for @mixin',
|
||||
$name_location ?: $class_location
|
||||
)
|
||||
)) {
|
||||
}
|
||||
} else {
|
||||
$storage->mixin_fqcln = Type::getFQCLNFromString(
|
||||
$docblock_info->mixin,
|
||||
$this->aliases
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$storage->sealed_properties = $docblock_info->sealed_properties;
|
||||
$storage->sealed_methods = $docblock_info->sealed_methods;
|
||||
|
||||
|
@ -96,6 +96,11 @@ class ClassLikeStorage
|
||||
*/
|
||||
public $psalm_internal = null;
|
||||
|
||||
/**
|
||||
* @var null|string
|
||||
*/
|
||||
public $mixin_fqcln = null;
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
|
77
tests/MixinAnnotationTest.php
Normal file
77
tests/MixinAnnotationTest.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
namespace Psalm\Tests;
|
||||
|
||||
use const DIRECTORY_SEPARATOR;
|
||||
use Psalm\Config;
|
||||
use Psalm\Context;
|
||||
|
||||
class MixinAnnotationTest extends TestCase
|
||||
{
|
||||
use Traits\ValidCodeAnalysisTestTrait;
|
||||
|
||||
/**
|
||||
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
|
||||
*/
|
||||
public function providerValidCodeParse()
|
||||
{
|
||||
return [
|
||||
'validSimpleAnnotations' => [
|
||||
'<?php
|
||||
class ParentClass {
|
||||
public function __call(string $name, array $args) {}
|
||||
}
|
||||
|
||||
class Provider {
|
||||
public function getString() : string {
|
||||
return "hello";
|
||||
}
|
||||
|
||||
public function setInteger(int $i) : void {}
|
||||
}
|
||||
|
||||
/** @mixin Provider */
|
||||
class Child extends ParentClass {}
|
||||
|
||||
$child = new Child();
|
||||
|
||||
$a = $child->getString();
|
||||
$child->setInteger(4);',
|
||||
'assertions' => [
|
||||
'$a' => 'string',
|
||||
],
|
||||
],
|
||||
'anotherSimpleExample' => [
|
||||
'<?php
|
||||
/**
|
||||
* @mixin B
|
||||
*/
|
||||
class A {
|
||||
/** @var B */
|
||||
private $b;
|
||||
|
||||
public function __construct() {
|
||||
$this->b = new B();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<mixed> $arguments
|
||||
* @return mixed
|
||||
*/
|
||||
public function __call(string $method, array $arguments)
|
||||
{
|
||||
return $this->b->$method(...$arguments);
|
||||
}
|
||||
}
|
||||
|
||||
class B {
|
||||
public function b(): void {
|
||||
echo "b";
|
||||
}
|
||||
}
|
||||
|
||||
$a = new A();
|
||||
$a->b();'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user