From 0c1564f4b5115293f28c15b9a42e7cffab643372 Mon Sep 17 00:00:00 2001 From: orklah Date: Mon, 13 Dec 2021 22:58:35 +0100 Subject: [PATCH 1/2] allow keyed array to contain class-strings --- src/Psalm/Internal/Type/ParseTreeCreator.php | 7 ++- src/Psalm/Internal/Type/TypeParser.php | 20 +++++++- src/Psalm/Type/Atomic/TKeyedArray.php | 51 ++++++++++++++------ 3 files changed, 60 insertions(+), 18 deletions(-) diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index 2c7c0e5e08f..64cbc2d6d84 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -792,9 +792,12 @@ private function handleValue(array $type_token): void case '::': $nexter_token = $this->t + 2 < $this->type_token_count ? $this->type_tokens[$this->t + 2] : null; - if ($this->current_leaf instanceof KeyedArrayTree) { + if ($this->current_leaf instanceof ParseTree\KeyedArrayTree + && $nexter_token + && strtolower($nexter_token[0]) !== 'class' + ) { throw new TypeParseTreeException( - 'Unexpected :: in array key' + ':: in array key is only allowed for ::class' ); } diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 31edcb382f5..2ca8fd411a9 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -1242,12 +1242,15 @@ private static function getTypeFromKeyedArrayTree( array $type_aliases ) { $properties = []; + $class_strings = []; $type = $parse_tree->value; $is_tuple = true; foreach ($parse_tree->children as $i => $property_branch) { + $class_string = false; + if (!$property_branch instanceof KeyedArrayPropertyTree) { $property_type = self::getTypeFromTree( $property_branch, @@ -1267,7 +1270,17 @@ private static function getTypeFromKeyedArrayTree( $type_aliases ); $property_maybe_undefined = $property_branch->possibly_undefined; - $property_key = $property_branch->value; + if (strpos($property_branch->value, '::')) { + [$fq_classlike_name, $const_name] = explode('::', $property_branch->value); + if ($const_name === 'class') { + $property_key = $fq_classlike_name; + $class_string = true; + } else { + $property_key = $property_branch->value; + } + } else { + $property_key = $property_branch->value; + } $is_tuple = false; } else { throw new TypeParseTreeException( @@ -1288,6 +1301,9 @@ private static function getTypeFromKeyedArrayTree( } $properties[$property_key] = $property_type; + if ($class_string) { + $class_strings[$property_key] = true; + } } if ($type !== 'array' && $type !== 'object' && $type !== 'callable-array') { @@ -1306,7 +1322,7 @@ private static function getTypeFromKeyedArrayTree( return new TCallableKeyedArray($properties); } - $object_like = new TKeyedArray($properties); + $object_like = new TKeyedArray($properties, $class_strings); if ($is_tuple) { $object_like->sealed = true; diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index db9c1241a59..eaca445f6dc 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -92,11 +92,14 @@ function ($name, Union $type): string { return (string) $type; } - if (is_string($name) && preg_match('/[ "\'\\\\.\n:]/', $name)) { - $name = '\'' . str_replace("\n", '\n', addslashes($name)) . '\''; + $class_string_suffix = ''; + if (isset($this->class_strings[$name])) { + $class_string_suffix = '::class'; } - return $name . ($type->possibly_undefined ? '?' : '') . ': ' . $type; + $name = $this->escapeAndQuote($name); + + return $name . $class_string_suffix . ($type->possibly_undefined ? '?' : '') . ': ' . $type; }, array_keys($this->properties), $this->properties @@ -118,11 +121,14 @@ function ($name, Union $type): string { return $type->getId(); } - if (is_string($name) && preg_match('/[ "\'\\\\.\n:]/', $name)) { - $name = '\'' . str_replace("\n", '\n', addslashes($name)) . '\''; + $class_string_suffix = ''; + if (isset($this->class_strings[$name])) { + $class_string_suffix = '::class'; } - return $name . ($type->possibly_undefined ? '?' : '') . ': ' . $type->getId(); + $name = $this->escapeAndQuote($name); + + return $name . $class_string_suffix . ($type->possibly_undefined ? '?' : '') . ': ' . $type->getId(); }, array_keys($this->properties), $this->properties @@ -177,16 +183,20 @@ function ( $this_class, $use_phpdoc_format ): string { - if (is_string($name) && preg_match('/[ "\'\\\\.\n:]/', $name)) { - $name = '\'' . str_replace("\n", '\n', addslashes($name)) . '\''; + $class_string_suffix = ''; + if (isset($this->class_strings[$name])) { + $class_string_suffix = '::class'; } - return $name . ($type->possibly_undefined ? '?' : '') . ': ' . $type->toNamespacedString( - $namespace, - $aliased_classes, - $this_class, - $use_phpdoc_format - ); + $name = $this->escapeAndQuote($name); + + return $name . $class_string_suffix . ($type->possibly_undefined ? '?' : '') . ': ' . + $type->toNamespacedString( + $namespace, + $aliased_classes, + $this_class, + $use_phpdoc_format + ); }, array_keys($this->properties), $this->properties @@ -412,4 +422,17 @@ public function getList(): TNonEmptyList return new TNonEmptyList($this->getGenericValueType()); } + + /** + * @param string|int $name + * @return string|int + */ + private function escapeAndQuote($name) + { + if (is_string($name) && preg_match('/[ "\'\\\\.\n:]/', $name)) { + $name = '\'' . str_replace("\n", '\n', addslashes($name)) . '\''; + } + + return $name; + } } From bb687aebbad7e8b2443dbd3df4cdfb2edd3806a3 Mon Sep 17 00:00:00 2001 From: orklah Date: Mon, 13 Dec 2021 23:16:27 +0100 Subject: [PATCH 2/2] add test --- tests/AssertAnnotationTest.php | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index 94ff4f8e5c3..f57925f6998 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -1657,6 +1657,33 @@ function c(\Aclass $item): bool { echo strlen($a->b->c); }', ], + 'assertOnKeyedArrayWithClassStringOffset' => [ + ' ""]; + + /** @var array $b */ + $b = []; + + $this->assertSame($a, $b); + } + + /** + * @template T + * @param T $expected + * @param mixed $actual + * @psalm-assert =T $actual + */ + public function assertSame($expected, $actual): void + { + return; + } + }', + ], ]; }