Skip to content

Commit

Permalink
Merge pull request #7152 from orklah/7109
Browse files Browse the repository at this point in the history
display class-strings in keyed arrays syntax and allow using them for assertions
  • Loading branch information
orklah committed Dec 15, 2021
2 parents 38a3efe + bb687ae commit 2dfe45a
Show file tree
Hide file tree
Showing 4 changed files with 87 additions and 18 deletions.
7 changes: 5 additions & 2 deletions src/Psalm/Internal/Type/ParseTreeCreator.php
Expand Up @@ -793,9 +793,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'
);
}

Expand Down
20 changes: 18 additions & 2 deletions src/Psalm/Internal/Type/TypeParser.php
Expand Up @@ -1243,12 +1243,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,
Expand All @@ -1268,7 +1271,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(
Expand All @@ -1289,6 +1302,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') {
Expand All @@ -1307,7 +1323,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;
Expand Down
51 changes: 37 additions & 14 deletions src/Psalm/Type/Atomic/TKeyedArray.php
Expand Up @@ -93,11 +93,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
Expand All @@ -119,11 +122,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
Expand Down Expand Up @@ -178,16 +184,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
Expand Down Expand Up @@ -413,4 +423,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;
}
}
27 changes: 27 additions & 0 deletions tests/AssertAnnotationTest.php
Expand Up @@ -1657,6 +1657,33 @@ function c(\Aclass $item): bool {
echo strlen($a->b->c);
}',
],
'assertOnKeyedArrayWithClassStringOffset' => [
'<?php
class A
{
function test(): void
{
$a = [stdClass::class => ""];
/** @var array<class-string, mixed> $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;
}
}',
],
];
}

Expand Down

0 comments on commit 2dfe45a

Please sign in to comment.