Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(types): add properties-of<T> type #7359

Merged
merged 9 commits into from Feb 28, 2022
1 change: 1 addition & 0 deletions docs/annotating_code/type_syntax/atomic_types.md
Expand Up @@ -52,6 +52,7 @@ Atomic types are the basic building block of all type information used in Psalm.
- `key-of<Foo\Bar::ARRAY_CONST>`
- `value-of<Foo\Bar::ARRAY_CONST>`
- `T[K]`
- [`properties-of<T>`](utility_types.md#properties-oft)

## Top types, bottom types

Expand Down
40 changes: 40 additions & 0 deletions docs/annotating_code/type_syntax/utility_types.md
@@ -0,0 +1,40 @@
# Utility types
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where are key-of and value-of documented? Maybe they should be added here? Maybe SomeClass::CONST_* as well? Probably others?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add these as well in another PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added key-of and value-of, SomeClass::CONST_* also could need some documentation, but I think it's not a utility type if you look at typescripts definition (https://www.typescriptlang.org/docs/handbook/utility-types.html)


Psalm supports some _magical_ utility types that brings superpower to the PHP type system.

## `properties-of<T>`

This collection of _utility types_ construct a keyed-array, with the names of non-static properties of a class as keys,
and their respective types as values. This can be useful if you need to convert objects into arrays.

```php
class A {
public string $foo;
public int $bar;

/**
* @return properties-of<self>
*/
public function asArray(): array {
return [
'foo' => $this->foo,
'bar' => $this->bar,
];
}

/**
* @return list<key-of<properties-of<self>>>
*/
public function attributeNames(): array {
return ['foo', 'bar']
}
}
```

### Variants

Note that `properties-of<T>` will return **all non-static** properties. There are the following subtypes to pick only
properties with a certain visibility:
- `public-properties-of<T>`
- `protected-properties-of<T>`
- `private-properties-of<T>`
125 changes: 116 additions & 9 deletions src/Psalm/Internal/Type/TypeExpander.php
Expand Up @@ -5,7 +5,11 @@
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;
Expand All @@ -27,6 +31,7 @@
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;
Expand All @@ -38,7 +43,6 @@
use function array_keys;
use function array_map;
use function array_merge;
use function array_push;
use function array_values;
use function count;
use function get_class;
Expand All @@ -55,7 +59,6 @@ class TypeExpander
{
/**
* @param string|TNamedObject|TTemplateParam|null $static_class_type
*
*/
public static function expandUnion(
Codebase $codebase,
Expand Down Expand Up @@ -302,6 +305,15 @@ public static function expandAtomic(
return [$return_type];
}

if ($return_type instanceof TPropertiesOf) {
return self::expandPropertiesOf(
$codebase,
$return_type,
$self_class,
$static_class_type
);
}

if ($return_type instanceof TTypeAlias) {
$declaring_fq_classlike_name = $return_type->declaring_fq_classlike_name;

Expand Down Expand Up @@ -358,8 +370,14 @@ public static function expandAtomic(
$codebase,
$return_type,
$self_class,
$static_class_type,
$parent_class,
$evaluate_class_constants,
$throw_on_unresolvable_constant
$evaluate_conditional_types,
$final,
$expand_generic,
$expand_templates,
$throw_on_unresolvable_constant,
);
}

Expand Down Expand Up @@ -877,22 +895,111 @@ private static function expandConditional(
return [$return_type];
}

/**
* @param string|TNamedObject|TTemplateParam|null $static_class_type
* @return non-empty-list<Atomic>
*/
private static function expandPropertiesOf(
Codebase $codebase,
TPropertiesOf $return_type,
?string $self_class,
$static_class_type
): array {
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 only non-static properties, and check visibility filter
$properties_types = array_filter(
$properties_types,
function (PropertyStorage $property) use ($return_type): bool {
if ($return_type->visibility_filter !== null
&& $property->visibility !== $return_type->visibility_filter
) {
return false;
}
return !$property->is_static;
}
);

// Return property names as literal string
$properties = array_map(
function (PropertyStorage $property): ?Union {
return $property->type;
},
$properties_types
);
$properties = array_filter(
$properties,
function (?Union $property_type): bool {
return $property_type !== null;
}
);

if ($properties === []) {
return [$return_type];
}
return [new TKeyedArray($properties)];
}

/**
* @param TKeyOfArray|TValueOfArray $return_type
* @param string|TNamedObject|TTemplateParam|null $static_class_type
* @return non-empty-list<Atomic>
*/
private static function expandKeyOfValueOfArray(
Codebase $codebase,
$return_type,
Atomic &$return_type,
?string $self_class,
bool $evaluate_class_constants,
bool $throw_on_unresolvable_constant
$static_class_type,
?string $parent_class,
bool $evaluate_class_constants = true,
bool $evaluate_conditional_types = false,
bool $final = false,
bool $expand_generic = false,
bool $expand_templates = false,
bool $throw_on_unresolvable_constant = false
): array {
// Expand class constants to their atomics
$type_atomics = [];
foreach ($return_type->type->getAtomicTypes() as $type_param) {
if (!$evaluate_class_constants || !$type_param instanceof TClassConstant) {
array_push($type_atomics, $type_param);
$type_param_expanded = self::expandAtomic(
$codebase,
$type_param,
$self_class,
$static_class_type,
$parent_class,
$evaluate_class_constants,
$evaluate_conditional_types,
$final,
$expand_generic,
$expand_templates,
$throw_on_unresolvable_constant,
);
$type_atomics = array_merge($type_atomics, $type_param_expanded);
continue;
}

Expand All @@ -901,8 +1008,8 @@ private static function expandKeyOfValueOfArray(
}

if ($throw_on_unresolvable_constant
&& !$codebase->classOrInterfaceExists($type_param->fq_classlike_name)
) {
&& !$codebase->classOrInterfaceExists($type_param->fq_classlike_name)
) {
throw new UnresolvableConstantException($type_param->fq_classlike_name, $type_param->const_name);
}

Expand Down
30 changes: 30 additions & 0 deletions src/Psalm/Internal/Type/TypeParser.php
Expand Up @@ -55,6 +55,7 @@
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;
Expand Down Expand Up @@ -685,6 +686,35 @@ private static function getTypeFromGenericTree(
);
}

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

Expand Down
4 changes: 4 additions & 0 deletions src/Psalm/Internal/Type/TypeTokenizer.php
Expand Up @@ -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,
Expand Down
1 change: 1 addition & 0 deletions src/Psalm/Type/Atomic/TKeyOfArray.php
Expand Up @@ -55,6 +55,7 @@ public static function isViableTemplateType(Union $template_type): bool
&& !$type instanceof TClassConstant
&& !$type instanceof TKeyedArray
&& !$type instanceof TList
&& !$type instanceof TPropertiesOf
) {
return false;
}
Expand Down
6 changes: 6 additions & 0 deletions src/Psalm/Type/Atomic/TKeyedArray.php
Expand Up @@ -10,6 +10,12 @@
use Psalm\Internal\Type\TypeCombiner;
use Psalm\Type;
use Psalm\Type\Atomic;
use Psalm\Type\Atomic\TArray;
use Psalm\Type\Atomic\TLiteralClassString;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TNonEmptyArray;
use Psalm\Type\Atomic\TNonEmptyList;
use Psalm\Type\Union;
use UnexpectedValueException;

Expand Down