Skip to content

Commit

Permalink
Fix #762 - support key-of and value-of types
Browse files Browse the repository at this point in the history
  • Loading branch information
muglug committed May 28, 2019
1 parent 076937c commit 7df8819
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 3 deletions.
47 changes: 45 additions & 2 deletions src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -999,13 +999,19 @@ public static function fleshOutType(
$new_return_type_parts = [];

foreach ($return_type->getTypes() as $return_type_part) {
$new_return_type_parts[] = self::fleshOutAtomicType(
$parts = self::fleshOutAtomicType(
$codebase,
$return_type_part,
$self_class,
$static_class_type,
$parent_class
);

if (is_array($parts)) {
$new_return_type_parts = array_merge($new_return_type_parts, $parts);
} else {
$new_return_type_parts[] = $parts;
}
}

$fleshed_out_type = new Type\Union($new_return_type_parts);
Expand All @@ -1026,7 +1032,7 @@ public static function fleshOutType(
* @param string|null $self_class
* @param string|Type\Atomic\TNamedObject|null $static_class_type
*
* @return Type\Atomic
* @return Type\Atomic|array<int, Type\Atomic>
*/
private static function fleshOutAtomicType(
Codebase $codebase,
Expand Down Expand Up @@ -1130,6 +1136,43 @@ private static function fleshOutAtomicType(
return $return_type;
}

if ($return_type instanceof Type\Atomic\TKeyOfClassConstant
|| $return_type instanceof Type\Atomic\TValueOfClassConstant
) {
if ($return_type->fq_classlike_name === 'self' && $self_class) {
$return_type->fq_classlike_name = $self_class;
}

if ($codebase->classOrInterfaceExists($return_type->fq_classlike_name)) {
$class_constants = $codebase->classlikes->getConstantsForClass(
$return_type->fq_classlike_name,
\ReflectionProperty::IS_PRIVATE
);

if (isset($class_constants[$return_type->const_name])) {
$const_type = $class_constants[$return_type->const_name];

foreach ($const_type->getTypes() as $const_type_atomic) {
if ($const_type_atomic instanceof Type\Atomic\ObjectLike
|| $const_type_atomic instanceof Type\Atomic\TArray
) {
if ($const_type_atomic instanceof Type\Atomic\ObjectLike) {
$const_type_atomic = $const_type_atomic->getGenericArrayType();
}

if ($return_type instanceof Type\Atomic\TKeyOfClassConstant) {
return array_values($const_type_atomic->type_params[0]->getTypes());
}

return array_values($const_type_atomic->type_params[1]->getTypes());
}
}
}
}

return $return_type;
}

if ($return_type instanceof Type\Atomic\TArray || $return_type instanceof Type\Atomic\TGenericObject) {
foreach ($return_type->type_params as &$type_param) {
$type_param = self::fleshOutType(
Expand Down
31 changes: 31 additions & 0 deletions src/Psalm/Type.php
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ abstract class Type
'never-returns' => true,
'array-key' => true,
'key-of' => true,
'value-of' => true,
];

/**
Expand Down Expand Up @@ -299,6 +300,36 @@ function (ParseTree $child_tree) use ($template_type_map) {
);
}

if ($generic_type_value === 'value-of') {
$param_name = (string) $generic_params[0];

if (isset($template_type_map[$param_name])) {
$defining_class = array_keys($template_type_map[$param_name])[0];

return new Atomic\TTemplateKeyOf(
$param_name,
$defining_class
);
}

$param_union_types = array_values($generic_params[0]->getTypes());

if (count($param_union_types) > 1) {
throw new TypeParseTreeException('Union types are not allowed in value-of type');
}

if (!$param_union_types[0] instanceof Atomic\TScalarClassConstant) {
throw new TypeParseTreeException(
'Untemplated value-of param ' . $param_name . ' should be a class constant'
);
}

return new Atomic\TValueOfClassConstant(
$param_union_types[0]->fq_classlike_name,
$param_union_types[0]->const_name
);
}

if (isset(self::PSALM_RESERVED_WORDS[$generic_type_value])
&& $generic_type_value !== 'self'
&& $generic_type_value !== 'static'
Expand Down
6 changes: 5 additions & 1 deletion src/Psalm/Type/Atomic/TKeyOfClassConstant.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,11 @@ public function toNamespacedString($namespace, array $aliased_classes, $this_cla
}

if (isset($aliased_classes[strtolower($this->fq_classlike_name)])) {
return 'key-of<' . $aliased_classes[strtolower($this->fq_classlike_name)] . '::' . $this->const_name . '>';
return 'key-of<'
. $aliased_classes[strtolower($this->fq_classlike_name)]
. '::'
. $this->const_name
. '>';
}

return 'key-of<\\' . $this->fq_classlike_name . '::' . $this->const_name . '>';
Expand Down
118 changes: 118 additions & 0 deletions src/Psalm/Type/Atomic/TValueOfClassConstant.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<?php
namespace Psalm\Type\Atomic;

class TValueOfClassConstant extends \Psalm\Type\Atomic
{
/** @var string */
public $fq_classlike_name;

/** @var string */
public $const_name;

/**
* @param string $fq_classlike_name
* @param string $const_name
*/
public function __construct($fq_classlike_name, $const_name)
{
$this->fq_classlike_name = $fq_classlike_name;
$this->const_name = $const_name;
}

/**
* @return string
*/
public function getKey()
{
return 'value-of<' . $this->fq_classlike_name . '::' . $this->const_name . '>';
}

/**
* @return string
*/
public function __toString()
{
return 'value-of<' . $this->fq_classlike_name . '::' . $this->const_name . '>';
}

/**
* @return string
*/
public function getId()
{
return $this->getKey();
}

/**
* @param string|null $namespace
* @param array<string> $aliased_classes
* @param string|null $this_class
* @param int $php_major_version
* @param int $php_minor_version
*
* @return string|null
*/
public function toPhpString(
$namespace,
array $aliased_classes,
$this_class,
$php_major_version,
$php_minor_version
) {
return null;
}

public function canBeFullyExpressedInPhp()
{
return false;
}

/**
* @param string|null $namespace
* @param array<string> $aliased_classes
* @param string|null $this_class
* @param bool $use_phpdoc_format
*
* @return string
*/
public function toNamespacedString($namespace, array $aliased_classes, $this_class, $use_phpdoc_format)
{
if ($this->fq_classlike_name === 'static') {
return 'value-of<static::' . $this->const_name . '>';
}

if ($this->fq_classlike_name === $this_class) {
return 'value-of<self::' . $this->const_name . '>';
}

if ($namespace && stripos($this->fq_classlike_name, $namespace . '\\') === 0) {
return 'value-of<' . preg_replace(
'/^' . preg_quote($namespace . '\\') . '/i',
'',
$this->fq_classlike_name
) . '::' . $this->const_name . '>';
}

if (!$namespace && stripos($this->fq_classlike_name, '\\') === false) {
return 'value-of<' . $this->fq_classlike_name . '::' . $this->const_name . '>';
}

if (isset($aliased_classes[strtolower($this->fq_classlike_name)])) {
return 'value-of<'
. $aliased_classes[strtolower($this->fq_classlike_name)]
. '::'
. $this->const_name
. '>';
}

return 'value-of<\\' . $this->fq_classlike_name . '::' . $this->const_name . '>';
}

/**
* @return string
*/
public function getAssertionString()
{
return 'mixed';
}
}
11 changes: 11 additions & 0 deletions tests/TypeParseTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,17 @@ public function testIndexedAccess()
);
}

/**
* @return void
*/
public function testValueOfClassConstant()
{
$this->assertSame(
'value-of<Foo\Baz::BAR>',
(string)Type::parseString('value-of<Foo\Baz::BAR>')
);
}

/**
* @return void
*/
Expand Down
30 changes: 30 additions & 0 deletions tests/ValueTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,36 @@ public function foo(string ...$things) : void {
}
',
],
'keyOf' => [
'<?php
class A {
const C = [
1 => "a",
2 => "b",
3 => "c"
];
/**
* @param key-of<A::C> $i
*/
public static function foo(int $i) : void {}
}'
],
'valueOf' => [
'<?php
class A {
const C = [
1 => "a",
2 => "b",
3 => "c"
];
/**
* @param value-of<A::C> $j
*/
public static function bar(string $j) : void {}
}'
],
];
}

Expand Down

0 comments on commit 7df8819

Please sign in to comment.