From 7df88197ef05af7e12f3d1782dc911fb8f66d3fd Mon Sep 17 00:00:00 2001 From: Brown Date: Tue, 28 May 2019 10:44:04 -0400 Subject: [PATCH] Fix #762 - support key-of and value-of types --- .../Statements/ExpressionAnalyzer.php | 47 ++++++- src/Psalm/Type.php | 31 +++++ src/Psalm/Type/Atomic/TKeyOfClassConstant.php | 6 +- .../Type/Atomic/TValueOfClassConstant.php | 118 ++++++++++++++++++ tests/TypeParseTest.php | 11 ++ tests/ValueTest.php | 30 +++++ 6 files changed, 240 insertions(+), 3 deletions(-) create mode 100644 src/Psalm/Type/Atomic/TValueOfClassConstant.php diff --git a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php index a6fe923daf8..85f5e08021a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ExpressionAnalyzer.php @@ -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); @@ -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 */ private static function fleshOutAtomicType( Codebase $codebase, @@ -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( diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index f2eff82fbdd..cf231262442 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -76,6 +76,7 @@ abstract class Type 'never-returns' => true, 'array-key' => true, 'key-of' => true, + 'value-of' => true, ]; /** @@ -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' diff --git a/src/Psalm/Type/Atomic/TKeyOfClassConstant.php b/src/Psalm/Type/Atomic/TKeyOfClassConstant.php index 92c9325e542..3deeae99d81 100644 --- a/src/Psalm/Type/Atomic/TKeyOfClassConstant.php +++ b/src/Psalm/Type/Atomic/TKeyOfClassConstant.php @@ -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 . '>'; diff --git a/src/Psalm/Type/Atomic/TValueOfClassConstant.php b/src/Psalm/Type/Atomic/TValueOfClassConstant.php new file mode 100644 index 00000000000..23c876d183d --- /dev/null +++ b/src/Psalm/Type/Atomic/TValueOfClassConstant.php @@ -0,0 +1,118 @@ +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 $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 $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-ofconst_name . '>'; + } + + if ($this->fq_classlike_name === $this_class) { + return 'value-ofconst_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'; + } +} diff --git a/tests/TypeParseTest.php b/tests/TypeParseTest.php index 3e0c0b92772..15639d38967 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -746,6 +746,17 @@ public function testIndexedAccess() ); } + /** + * @return void + */ + public function testValueOfClassConstant() + { + $this->assertSame( + 'value-of', + (string)Type::parseString('value-of') + ); + } + /** * @return void */ diff --git a/tests/ValueTest.php b/tests/ValueTest.php index dfad1aca648..349862ef8b4 100644 --- a/tests/ValueTest.php +++ b/tests/ValueTest.php @@ -593,6 +593,36 @@ public function foo(string ...$things) : void { } ', ], + 'keyOf' => [ + ' "a", + 2 => "b", + 3 => "c" + ]; + + /** + * @param key-of $i + */ + public static function foo(int $i) : void {} + }' + ], + 'valueOf' => [ + ' "a", + 2 => "b", + 3 => "c" + ]; + + /** + * @param value-of $j + */ + public static function bar(string $j) : void {} + }' + ], ]; }