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

display class-strings in keyed arrays syntax and allow using them for assertions #7152

Merged
merged 2 commits into from Dec 15, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/Psalm/Internal/Type/ParseTreeCreator.php
Expand Up @@ -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'
);
}

Expand Down
20 changes: 18 additions & 2 deletions src/Psalm/Internal/Type/TypeParser.php
Expand Up @@ -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,
Expand All @@ -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(
Expand All @@ -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') {
Expand All @@ -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;
Expand Down
51 changes: 37 additions & 14 deletions src/Psalm/Type/Atomic/TKeyedArray.php
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
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