mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
feat(types): add properties-of<T> type
This commit is contained in:
parent
997bded2e3
commit
30fac906c6
@ -5,7 +5,11 @@ namespace Psalm\Internal\Type;
|
||||
use Psalm\Codebase;
|
||||
use Psalm\Exception\CircularReferenceException;
|
||||
use Psalm\Exception\UnresolvableConstantException;
|
||||
use Psalm\Internal\Type\SimpleAssertionReconciler;
|
||||
use Psalm\Internal\Type\SimpleNegatedAssertionReconciler;
|
||||
use Psalm\Internal\Type\TypeParser;
|
||||
use Psalm\Storage\Assertion\IsType;
|
||||
use Psalm\Storage\PropertyStorage;
|
||||
use Psalm\Type\Atomic;
|
||||
use Psalm\Type\Atomic\TArray;
|
||||
use Psalm\Type\Atomic\TCallable;
|
||||
@ -23,10 +27,12 @@ use Psalm\Type\Atomic\TKeyedArray;
|
||||
use Psalm\Type\Atomic\TList;
|
||||
use Psalm\Type\Atomic\TLiteralClassString;
|
||||
use Psalm\Type\Atomic\TLiteralInt;
|
||||
use Psalm\Type\Atomic\TLiteralString;
|
||||
use Psalm\Type\Atomic\TNamedObject;
|
||||
use Psalm\Type\Atomic\TNever;
|
||||
use Psalm\Type\Atomic\TNull;
|
||||
use Psalm\Type\Atomic\TObjectWithProperties;
|
||||
use Psalm\Type\Atomic\TPropertiesOf;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
use Psalm\Type\Atomic\TTypeAlias;
|
||||
use Psalm\Type\Atomic\TValueOfArray;
|
||||
@ -302,6 +308,56 @@ class TypeExpander
|
||||
return [$return_type];
|
||||
}
|
||||
|
||||
if ($return_type instanceof TPropertiesOf) {
|
||||
if ($return_type->fq_classlike_name === 'self' && $self_class) {
|
||||
$return_type->fq_classlike_name = $self_class;
|
||||
}
|
||||
|
||||
if ($return_type->fq_classlike_name === 'static' && $self_class) {
|
||||
$return_type->fq_classlike_name = is_string($static_class_type) ? $static_class_type : $self_class;
|
||||
}
|
||||
|
||||
if (!$codebase->classExists($return_type->fq_classlike_name)) {
|
||||
return $return_type;
|
||||
}
|
||||
|
||||
// Get and merge all properties from parent classes
|
||||
$class_storage = $codebase->classlike_storage_provider->get($return_type->fq_classlike_name);
|
||||
$properties_types = $class_storage->properties;
|
||||
foreach ($class_storage->parent_classes as $parent_class) {
|
||||
if (!$codebase->classOrInterfaceExists($parent_class)) {
|
||||
continue;
|
||||
}
|
||||
$parent_class_storage = $codebase->classlike_storage_provider->get($parent_class);
|
||||
$properties_types = array_merge(
|
||||
$properties_types,
|
||||
$parent_class_storage->properties
|
||||
);
|
||||
}
|
||||
|
||||
// Filter properties if configured
|
||||
if ($return_type->visibility_filter !== null) {
|
||||
$properties_types = array_filter(
|
||||
$properties_types,
|
||||
function (PropertyStorage $property) use ($return_type): bool {
|
||||
return $property->visibility === $return_type->visibility_filter;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Return property names as literal string
|
||||
$properties = array_map(
|
||||
function (string $property_name): TLiteralString {
|
||||
return new TLiteralString($property_name);
|
||||
},
|
||||
array_keys($properties_types)
|
||||
);
|
||||
if (empty($properties)) {
|
||||
return $return_type;
|
||||
}
|
||||
return $properties;
|
||||
}
|
||||
|
||||
if ($return_type instanceof TTypeAlias) {
|
||||
$declaring_fq_classlike_name = $return_type->declaring_fq_classlike_name;
|
||||
|
||||
|
@ -55,6 +55,7 @@ use Psalm\Type\Atomic\TNonEmptyList;
|
||||
use Psalm\Type\Atomic\TNull;
|
||||
use Psalm\Type\Atomic\TObject;
|
||||
use Psalm\Type\Atomic\TObjectWithProperties;
|
||||
use Psalm\Type\Atomic\TPropertiesOf;
|
||||
use Psalm\Type\Atomic\TTemplateIndexedAccess;
|
||||
use Psalm\Type\Atomic\TTemplateKeyOf;
|
||||
use Psalm\Type\Atomic\TTemplateParam;
|
||||
@ -685,6 +686,35 @@ class TypeParser
|
||||
);
|
||||
}
|
||||
|
||||
if (in_array($generic_type_value, TPropertiesOf::tokenNames())) {
|
||||
if (count($generic_params) !== 1) {
|
||||
throw new TypeParseTreeException($generic_type_value . ' requires exactly one parameter.');
|
||||
}
|
||||
|
||||
$class_name = (string) $generic_params[0];
|
||||
|
||||
if (isset($template_type_map[$class_name])) {
|
||||
throw new TypeParseTreeException('Template types are not allowed in ' . $generic_type_value . ' param');
|
||||
}
|
||||
|
||||
|
||||
$param_union_types = array_values($generic_params[0]->getAtomicTypes());
|
||||
|
||||
if (count($param_union_types) > 1) {
|
||||
throw new TypeParseTreeException('Union types are not allowed in ' . $generic_type_value . ' param');
|
||||
}
|
||||
|
||||
if (!$param_union_types[0] instanceof TNamedObject) {
|
||||
throw new TypeParseTreeException('Param should be a named object in ' . $generic_type_value);
|
||||
}
|
||||
|
||||
return new TPropertiesOf(
|
||||
$class_name,
|
||||
$param_union_types[0],
|
||||
TPropertiesOf::filterForTokenName($generic_type_value)
|
||||
);
|
||||
}
|
||||
|
||||
if ($generic_type_value === 'key-of') {
|
||||
$param_name = $generic_params[0]->getId(false);
|
||||
|
||||
|
@ -78,6 +78,10 @@ class TypeTokenizer
|
||||
'array-key' => true,
|
||||
'key-of' => true,
|
||||
'value-of' => true,
|
||||
'properties-of' => true,
|
||||
'public-properties-of' => true,
|
||||
'protected-properties-of' => true,
|
||||
'private-properties-of' => true,
|
||||
'non-empty-countable' => true,
|
||||
'list' => true,
|
||||
'non-empty-list' => true,
|
||||
|
126
src/Psalm/Type/Atomic/TPropertiesOf.php
Normal file
126
src/Psalm/Type/Atomic/TPropertiesOf.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Type\Atomic;
|
||||
|
||||
use Psalm\Type\Atomic;
|
||||
|
||||
/**
|
||||
* Type that resolves to property names of a class/interface.
|
||||
*
|
||||
* @psalm-type TokenName = 'properties-of'|'public-properties-of'|'protected-properties-of'|'private-properties-of'
|
||||
*/
|
||||
class TPropertiesOf extends Atomic
|
||||
{
|
||||
// These should match the values of
|
||||
// `Psalm\Internal\Analyzer\ClassLikeAnalyzer::VISIBILITY_*`, as they are
|
||||
// used to compared against properties visibililty.
|
||||
public const VISIBILITY_PUBLIC = 1;
|
||||
public const VISIBILITY_PROTECTED = 2;
|
||||
public const VISIBILITY_PRIVATE = 3;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
public $fq_classlike_name;
|
||||
/**
|
||||
* @var TNamedObject
|
||||
*/
|
||||
public $classlike_type;
|
||||
/**
|
||||
* @var self::VISIBILITY_*|null
|
||||
*/
|
||||
public $visibility_filter;
|
||||
|
||||
/**
|
||||
* @return list<TokenName>
|
||||
*/
|
||||
public static function tokenNames(): array
|
||||
{
|
||||
return [
|
||||
'properties-of',
|
||||
'public-properties-of',
|
||||
'protected-properties-of',
|
||||
'private-properties-of'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param TokenName $tokenName
|
||||
* @return self::VISIBILITY_*|null
|
||||
*/
|
||||
public static function filterForTokenName(string $token_name): ?int
|
||||
{
|
||||
switch ($token_name) {
|
||||
case 'public-properties-of':
|
||||
return self::VISIBILITY_PUBLIC;
|
||||
case 'protected-properties-of':
|
||||
return self::VISIBILITY_PROTECTED;
|
||||
case 'private-properties-of':
|
||||
return self::VISIBILITY_PRIVATE;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TokenName
|
||||
*/
|
||||
private static function tokenNameForFilter(?int $visibility_filter): string
|
||||
{
|
||||
switch ($visibility_filter) {
|
||||
case self::VISIBILITY_PUBLIC:
|
||||
return 'public-properties-of';
|
||||
case self::VISIBILITY_PROTECTED:
|
||||
return 'protected-properties-of';
|
||||
case self::VISIBILITY_PRIVATE:
|
||||
return 'private-properties-of';
|
||||
default:
|
||||
return 'properties-of';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a new instance of a generic type
|
||||
*
|
||||
* @param self::VISIBILITY_*|null $visibility_filter
|
||||
*/
|
||||
public function __construct(string $fq_classlike_name, TNamedObject $classlike_type, ?int $visibility_filter = null)
|
||||
{
|
||||
$this->fq_classlike_name = $fq_classlike_name;
|
||||
$this->classlike_type = $classlike_type;
|
||||
$this->visibility_filter = $visibility_filter;
|
||||
}
|
||||
|
||||
public function getKey(bool $include_extra = true): string
|
||||
{
|
||||
return self::tokenNameForFilter($this->visibility_filter) . '<' . $this->classlike_type . '>';
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getKey();
|
||||
}
|
||||
|
||||
public function getId(bool $nested = false): string
|
||||
{
|
||||
return $this->getKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<lowercase-string, string> $aliased_classes
|
||||
*/
|
||||
public function toPhpString(
|
||||
?string $namespace,
|
||||
array $aliased_classes,
|
||||
?string $this_class,
|
||||
int $php_major_version,
|
||||
int $php_minor_version
|
||||
): string {
|
||||
return $this->getKey();
|
||||
}
|
||||
|
||||
public function canBeFullyExpressedInPhp(int $php_major_version, int $php_minor_version): bool
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
330
tests/PropertiesOfTest.php
Normal file
330
tests/PropertiesOfTest.php
Normal file
@ -0,0 +1,330 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Tests;
|
||||
|
||||
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
|
||||
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
|
||||
|
||||
class PropertiesOfTest extends TestCase
|
||||
{
|
||||
use ValidCodeAnalysisTestTrait;
|
||||
use InvalidCodeAnalysisTestTrait;
|
||||
|
||||
/**
|
||||
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
|
||||
*/
|
||||
public function providerValidCodeParse(): iterable
|
||||
{
|
||||
return [
|
||||
'varStatement' => [
|
||||
'<?php
|
||||
class A {
|
||||
public int $foo = 42;
|
||||
}
|
||||
|
||||
/** @var properties-of<A> */
|
||||
$test = \'foo\';
|
||||
',
|
||||
],
|
||||
'returnStatement' => [
|
||||
'<?php
|
||||
class A {
|
||||
public int $foo = 42;
|
||||
}
|
||||
|
||||
/** @return properties-of<A> */
|
||||
function returnPropertyOfA() {
|
||||
return \'foo\';
|
||||
}
|
||||
',
|
||||
],
|
||||
'publicPropertiesOf' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var mixed */
|
||||
public $foo;
|
||||
/** @var mixed */
|
||||
private $bar;
|
||||
/** @var mixed */
|
||||
protected $adams;
|
||||
}
|
||||
|
||||
/** @return public-properties-of<A> */
|
||||
function returnPropertyOfA() {
|
||||
return \'foo\';
|
||||
}
|
||||
',
|
||||
],
|
||||
'protectedPropertiesOf' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var mixed */
|
||||
public $foo;
|
||||
/** @var mixed */
|
||||
private $bar;
|
||||
/** @var mixed */
|
||||
protected $adams;
|
||||
}
|
||||
|
||||
/** @return protected-properties-of<A> */
|
||||
function returnPropertyOfA() {
|
||||
return \'adams\';
|
||||
}
|
||||
',
|
||||
],
|
||||
'privatePropertiesOf' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var mixed */
|
||||
public $foo;
|
||||
/** @var mixed */
|
||||
private $bar;
|
||||
/** @var mixed */
|
||||
protected $adams;
|
||||
}
|
||||
|
||||
/** @return private-properties-of<A> */
|
||||
function returnPropertyOfA() {
|
||||
return \'bar\';
|
||||
}
|
||||
',
|
||||
],
|
||||
'allPropertiesOf' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var mixed */
|
||||
public $foo;
|
||||
/** @var mixed */
|
||||
private $bar;
|
||||
/** @var mixed */
|
||||
protected $adams;
|
||||
}
|
||||
|
||||
/** @return properties-of<A> */
|
||||
function returnPropertyOfA(int $visibility) {
|
||||
if ($visibility === 1) {
|
||||
return \'foo\';
|
||||
} elseif ($visibility === 2) {
|
||||
return \'bar\';
|
||||
} else {
|
||||
return \'adams\';
|
||||
}
|
||||
}
|
||||
',
|
||||
],
|
||||
'usePropertiesOfSelfAsArrayKey' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var int */
|
||||
public $a = 1;
|
||||
/** @var int */
|
||||
public $b = 2;
|
||||
|
||||
/** @return array<properties-of<self>, int> */
|
||||
public function asArray() {
|
||||
return [
|
||||
\'a\' => $this->a,
|
||||
\'b\' => $this->b
|
||||
];
|
||||
}
|
||||
}',
|
||||
],
|
||||
'usePropertiesOfStaticAsArrayKey' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var int */
|
||||
public $a = 1;
|
||||
/** @var int */
|
||||
public $b = 2;
|
||||
|
||||
/** @return array<properties-of<static>, int> */
|
||||
public function asArray() {
|
||||
return [
|
||||
\'a\' => $this->a,
|
||||
\'b\' => $this->b
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
/** @var int */
|
||||
public $c = 3;
|
||||
|
||||
public function asArray() {
|
||||
return [
|
||||
\'a\' => $this->a,
|
||||
\'b\' => $this->b,
|
||||
\'c\' => $this->c,
|
||||
];
|
||||
}
|
||||
}
|
||||
',
|
||||
],
|
||||
'propertiesOfMultipleInheritanceStaticAsArrayKey' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var int */
|
||||
public $a = 1;
|
||||
/** @var int */
|
||||
public $b = 2;
|
||||
|
||||
/** @return array<properties-of<static>, int> */
|
||||
public function asArray() {
|
||||
return [
|
||||
\'a\' => $this->a,
|
||||
\'b\' => $this->b
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class B extends A {
|
||||
/** @var int */
|
||||
public $c = 3;
|
||||
}
|
||||
|
||||
class C extends B {
|
||||
/** @var int */
|
||||
public $d = 4;
|
||||
|
||||
public function asArray() {
|
||||
return [
|
||||
\'a\' => $this->a,
|
||||
\'b\' => $this->b,
|
||||
\'c\' => $this->c,
|
||||
\'d\' => $this->d,
|
||||
];
|
||||
}
|
||||
}
|
||||
',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string,array{string,error_message:string,1?:string[],2?:bool,3?:string}>
|
||||
*/
|
||||
public function providerInvalidCodeParse(): iterable
|
||||
{
|
||||
return [
|
||||
'onlyOneTemplateParam' => [
|
||||
'<?php
|
||||
|
||||
class A {}
|
||||
class B {}
|
||||
|
||||
/** @var properties-of<A, B> */
|
||||
$test = \'foobar\';
|
||||
',
|
||||
'error_message' => 'InvalidDocblock',
|
||||
],
|
||||
'publicPropertiesOfPicksNoPrivate' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var mixed */
|
||||
public $foo;
|
||||
/** @var mixed */
|
||||
private $bar;
|
||||
/** @var mixed */
|
||||
protected $adams;
|
||||
}
|
||||
|
||||
/** @return public-properties-of<A> */
|
||||
function returnPropertyOfA() {
|
||||
return \'bar\';
|
||||
}
|
||||
',
|
||||
'error_message' => 'InvalidReturnStatement'
|
||||
],
|
||||
'publicPropertiesOfPicksNoProtected' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var mixed */
|
||||
public $foo;
|
||||
/** @var mixed */
|
||||
private $bar;
|
||||
/** @var mixed */
|
||||
protected $adams;
|
||||
}
|
||||
|
||||
/** @return public-properties-of<A> */
|
||||
function returnPropertyOfA() {
|
||||
return \'adams\';
|
||||
}
|
||||
',
|
||||
'error_message' => 'InvalidReturnStatement'
|
||||
],
|
||||
'protectedPropertiesOfPicksNoPublic' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var mixed */
|
||||
public $foo;
|
||||
/** @var mixed */
|
||||
private $bar;
|
||||
/** @var mixed */
|
||||
protected $adams;
|
||||
}
|
||||
|
||||
/** @return protected-properties-of<A> */
|
||||
function returnPropertyOfA() {
|
||||
return \'foo\';
|
||||
}
|
||||
',
|
||||
'error_message' => 'InvalidReturnStatement'
|
||||
],
|
||||
'protectedPropertiesOfPicksNoPrivate' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var mixed */
|
||||
public $foo;
|
||||
/** @var mixed */
|
||||
private $bar;
|
||||
/** @var mixed */
|
||||
protected $adams;
|
||||
}
|
||||
|
||||
/** @return protected-properties-of<A> */
|
||||
function returnPropertyOfA() {
|
||||
return \'bar\';
|
||||
}
|
||||
',
|
||||
'error_message' => 'InvalidReturnStatement'
|
||||
],
|
||||
'privatePropertiesOfPicksNoPublic' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var mixed */
|
||||
public $foo;
|
||||
/** @var mixed */
|
||||
private $bar;
|
||||
/** @var mixed */
|
||||
protected $adams;
|
||||
}
|
||||
|
||||
/** @return private-properties-of<A> */
|
||||
function returnPropertyOfA() {
|
||||
return \'foo\';
|
||||
}
|
||||
',
|
||||
'error_message' => 'InvalidReturnStatement'
|
||||
],
|
||||
'privatePropertiesOfPicksNoProtected' => [
|
||||
'<?php
|
||||
class A {
|
||||
/** @var mixed */
|
||||
public $foo;
|
||||
/** @var mixed */
|
||||
private $bar;
|
||||
/** @var mixed */
|
||||
protected $adams;
|
||||
}
|
||||
|
||||
/** @return private-properties-of<A> */
|
||||
function returnPropertyOfA() {
|
||||
return \'adams\';
|
||||
}
|
||||
',
|
||||
'error_message' => 'InvalidReturnStatement'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user