From d6aff3ed20598890c47255a85724fb763d679de7 Mon Sep 17 00:00:00 2001 From: shvlv Date: Tue, 24 Jan 2023 12:24:52 +0200 Subject: [PATCH 1/7] Parse class constant for PhpStorm Meta override --- .../Internal/Scanner/PhpStormMetaScanner.php | 33 +++++++++++++++++++ tests/StubTest.php | 16 +++++++++ tests/fixtures/stubs/phpstorm.meta.php | 7 ++++ 3 files changed, 56 insertions(+) diff --git a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php index 5870f4925..e49ba2baf 100644 --- a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php +++ b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php @@ -12,6 +12,7 @@ use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; +use ReflectionProperty; use function count; use function implode; @@ -63,6 +64,38 @@ class PhpStormMetaScanner } elseif ($array_item->value instanceof PhpParser\Node\Scalar\String_) { $map[$array_item->key->value] = $array_item->value->value; } + } elseif ($array_item->key instanceof PhpParser\Node\Expr\ClassConstFetch + && $array_item->key->class instanceof PhpParser\Node\Name\FullyQualified + && $array_item->key->name instanceof PhpParser\Node\Identifier + ) { + $resolved_name = $array_item->key->class->getAttribute('resolvedName'); + if (!$resolved_name) { + continue; + } + + $constant_type = $codebase->classlikes->getClassConstantType( + $resolved_name, + $array_item->key->name->name, + ReflectionProperty::IS_PUBLIC, + ); + + if (!$constant_type instanceof Union || !$constant_type->isSingleStringLiteral()) { + continue; + } + + $meta_key = $constant_type->getSingleStringLiteral()->value; + + if ($array_item->value instanceof PhpParser\Node\Expr\ClassConstFetch + && $array_item->value->class instanceof PhpParser\Node\Name\FullyQualified + && $array_item->value->name instanceof PhpParser\Node\Identifier + && strtolower($array_item->value->name->name) + ) { + $map[$meta_key] = new Union([ + new TNamedObject(implode('\\', $array_item->value->class->parts)), + ]); + } elseif ($array_item->value instanceof PhpParser\Node\Scalar\String_) { + $map[$meta_key] = $array_item->value->value; + } } } } diff --git a/tests/StubTest.php b/tests/StubTest.php index 8cb13b69e..41ff56d2a 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -317,6 +317,10 @@ class StubTest extends TestCase 'creAte2("object"); $y2 = (new \Ns\MyClass)->creaTe2("exception"); + + $const1 = (new \Ns\MyClass)->creAte3(\Ns\MyClass::OBJECT); + $const2 = (new \Ns\MyClass)->creaTe3("exception"); $b1 = \Create("object"); $b2 = \cReate("exception"); @@ -404,6 +417,9 @@ class StubTest extends TestCase '$y1===' => 'stdClass', '$y2===' => 'Exception', + '$const1===' => 'stdClass', + '$const2===' => 'Exception', + '$b1===' => 'stdClass', '$b2===' => 'Exception', diff --git a/tests/fixtures/stubs/phpstorm.meta.php b/tests/fixtures/stubs/phpstorm.meta.php index f2d3944f9..7ef519238 100644 --- a/tests/fixtures/stubs/phpstorm.meta.php +++ b/tests/fixtures/stubs/phpstorm.meta.php @@ -25,6 +25,13 @@ namespace PHPSTORM_META { 'object' => \stdClass::class, ])); + // tests with class constant as key + override(\Ns\MyClass::crEate3(), map([ + '' => '@', + \Ns\MyClass::EXCEPTION => \Exception::class, + \Ns\MyClass::OBJECT => \stdClass::class, + ])); + override(\Ns\MyClass::foO(0), type(0)); override(\Ns\MyClass::Bar(0), elementType(0)); override(\foo(0), type(0)); From 6227943bfbd10bb0d6c32f8a8b576353c2af9683 Mon Sep 17 00:00:00 2001 From: shvlv Date: Tue, 24 Jan 2023 13:31:58 +0200 Subject: [PATCH 2/7] Allow to scan private constants --- src/Psalm/Internal/Scanner/PhpStormMetaScanner.php | 2 +- tests/StubTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php index e49ba2baf..a70d82c57 100644 --- a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php +++ b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php @@ -76,7 +76,7 @@ class PhpStormMetaScanner $constant_type = $codebase->classlikes->getClassConstantType( $resolved_name, $array_item->key->name->name, - ReflectionProperty::IS_PUBLIC, + ReflectionProperty::IS_PRIVATE, ); if (!$constant_type instanceof Union || !$constant_type->isSingleStringLiteral()) { diff --git a/tests/StubTest.php b/tests/StubTest.php index 41ff56d2a..a86f14868 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -319,7 +319,7 @@ class StubTest extends TestCase class MyClass { public const OBJECT = "object"; - public const EXCEPTION = "exception"; + private const EXCEPTION = "exception"; /** * @return mixed From eca707998290376c757bd79f04356ea5664af464 Mon Sep 17 00:00:00 2001 From: shvlv Date: Wed, 25 Jan 2023 11:32:41 +0200 Subject: [PATCH 3/7] Fix Psalm errors --- src/Psalm/Internal/Scanner/PhpStormMetaScanner.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php index a70d82c57..5058e67ce 100644 --- a/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php +++ b/src/Psalm/Internal/Scanner/PhpStormMetaScanner.php @@ -64,10 +64,12 @@ class PhpStormMetaScanner } elseif ($array_item->value instanceof PhpParser\Node\Scalar\String_) { $map[$array_item->key->value] = $array_item->value->value; } - } elseif ($array_item->key instanceof PhpParser\Node\Expr\ClassConstFetch + } elseif ($array_item + && $array_item->key instanceof PhpParser\Node\Expr\ClassConstFetch && $array_item->key->class instanceof PhpParser\Node\Name\FullyQualified && $array_item->key->name instanceof PhpParser\Node\Identifier ) { + /** @var string|null $resolved_name */ $resolved_name = $array_item->key->class->getAttribute('resolvedName'); if (!$resolved_name) { continue; From 247d30f74c023108c6393933e1d26cbc63ccbdd7 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 25 Jan 2023 10:42:05 +0100 Subject: [PATCH 4/7] Detect duplicate keys in array shapes --- src/Psalm/Internal/Type/TypeParser.php | 12 ++++++++---- tests/TypeParseTest.php | 22 ++++++++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index f255618d9..a5635899c 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -1478,6 +1478,10 @@ class TypeParser $had_optional = true; } + if (isset($properties[$property_key])) { + throw new TypeParseTreeException("Duplicate key $property_key detected"); + } + $properties[$property_key] = $property_type; if ($class_string) { $class_strings[$property_key] = true; @@ -1485,7 +1489,7 @@ class TypeParser } if ($had_explicit && $had_implicit) { - throw new TypeParseTreeException('Cannot mix explicit and implicit keys!'); + throw new TypeParseTreeException('Cannot mix explicit and implicit keys'); } if ($type === 'object') { @@ -1500,7 +1504,7 @@ class TypeParser } if ($callable && !$properties) { - throw new TypeParseTreeException('A callable array cannot be empty!'); + throw new TypeParseTreeException('A callable array cannot be empty'); } if ($type !== 'array' && $type !== 'list') { @@ -1508,7 +1512,7 @@ class TypeParser } if ($type === 'list' && !$is_list) { - throw new TypeParseTreeException('A list shape cannot describe a non-list!'); + throw new TypeParseTreeException('A list shape cannot describe a non-list'); } if (!$properties) { @@ -1520,7 +1524,7 @@ class TypeParser $class_strings, $sealed ? null - : [$is_list ? Type::getInt() : Type::getArrayKey(), Type::getMixed()], + : [$is_list ? Type::getListKey() : Type::getArrayKey(), Type::getMixed()], $is_list, $from_docblock, ); diff --git a/tests/TypeParseTest.php b/tests/TypeParseTest.php index 07f7c68ce..81634a123 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -473,54 +473,60 @@ class TypeParseTest extends TestCase public function testTKeyedListNonList(): void { - $this->expectExceptionMessage('A list shape cannot describe a non-list!'); + $this->expectExceptionMessage('A list shape cannot describe a non-list'); Type::parseString('list{a: 0, b: 1, c: 2}'); } public function testTKeyedListNonListOptional(): void { - $this->expectExceptionMessage('A list shape cannot describe a non-list!'); + $this->expectExceptionMessage('A list shape cannot describe a non-list'); Type::parseString('list{a: 0, b?: 1, c?: 2}'); } public function testTKeyedListNonListOptionalWrongOrder1(): void { - $this->expectExceptionMessage('A list shape cannot describe a non-list!'); + $this->expectExceptionMessage('A list shape cannot describe a non-list'); Type::parseString('list{0?: 0, 1: 1, 2: 2}'); } public function testTKeyedListNonListOptionalWrongOrder2(): void { - $this->expectExceptionMessage('A list shape cannot describe a non-list!'); + $this->expectExceptionMessage('A list shape cannot describe a non-list'); Type::parseString('list{0: 0, 1?: 1, 2: 2}'); } public function testTKeyedListWrongOrder(): void { - $this->expectExceptionMessage('A list shape cannot describe a non-list!'); + $this->expectExceptionMessage('A list shape cannot describe a non-list'); Type::parseString('list{1: 1, 0: 0}'); } public function testTKeyedListNonListKeys(): void { - $this->expectExceptionMessage('A list shape cannot describe a non-list!'); + $this->expectExceptionMessage('A list shape cannot describe a non-list'); Type::parseString('list{1: 1, 2: 2}'); } public function testTKeyedListNoExplicitAndImplicitKeys(): void { - $this->expectExceptionMessage('Cannot mix explicit and implicit keys!'); + $this->expectExceptionMessage('Cannot mix explicit and implicit keys'); Type::parseString('list{0: 0, 1}'); } public function testTKeyedArrayNoExplicitAndImplicitKeys(): void { - $this->expectExceptionMessage('Cannot mix explicit and implicit keys!'); + $this->expectExceptionMessage('Cannot mix explicit and implicit keys'); Type::parseString('array{0, test: 1}'); } + public function testTKeyedArrayNoDuplicateKeys(): void + { + $this->expectExceptionMessage('Duplicate key a detected'); + Type::parseString('array{a: int, a: int}'); + } + public function testSimpleCallable(): void { $this->assertSame( From 40a05d574ae246a1a3da6de581566fff990d817c Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Wed, 25 Jan 2023 12:26:59 +0100 Subject: [PATCH 5/7] Improve error messages --- src/Psalm/Internal/Analyzer/CommentAnalyzer.php | 2 +- .../Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index 805da9340..7d8fdd13a 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -162,7 +162,7 @@ class CommentAnalyzer throw new DocblockParseException( $line_parts[0] . ' is not a valid type' . - ' (from ' . + ' ('.$e->getMessage().' in ' . $source->getFilePath() . ':' . $comment->getStartLine() . diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 1495cdcb2..3da672a63 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -1895,7 +1895,7 @@ class ClassLikeNodeScanner $self_fqcln, ); } catch (TypeParseTreeException $e) { - throw new DocblockParseException($type_string . ' is not a valid type'); + throw new DocblockParseException($type_string . ' is not a valid type: '.$e->getMessage()); } $type_alias_tokens[$type_alias] = new InlineTypeAlias($type_tokens); From 32581a71fd94767dcf3d4731f6902ba58f25e921 Mon Sep 17 00:00:00 2001 From: Jack Worman Date: Wed, 25 Jan 2023 10:54:33 -0500 Subject: [PATCH 6/7] cdata in baseline --- psalm-baseline.xml | 180 ++++++++++++++++++------------------ src/Psalm/ErrorBaseline.php | 7 +- 2 files changed, 96 insertions(+), 91 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 39e01182c..eed99a123 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,14 +1,14 @@ - + - $comment_block->tags['variablesfrom'][0] + tags['variablesfrom'][0]]]> $matches[1] - $comment_block->tags['variablesfrom'][0] + tags['variablesfrom'][0]]]> $matches[1] @@ -24,7 +24,7 @@ - explode('::', $method_id)[1] + @@ -38,7 +38,7 @@ $comments[0] $property_name - $stmt->props[0] + props[0]]]> $uninitialized_variables[0] @@ -58,7 +58,7 @@ - $stmt->cond + cond]]> @@ -69,46 +69,46 @@ - $context->assigned_var_ids += $switch_scope->new_assigned_var_ids + assigned_var_ids += $switch_scope->new_assigned_var_ids]]> - $new_case_equality_expr->getArgs()[1] - $switch_scope->leftover_statements[0] - $traverser->traverse([$switch_condition])[0] + getArgs()[1]]]> + leftover_statements[0]]]> + traverse([$switch_condition])[0]]]> - $assertion->rule[0] - $assertion->rule[0] - $assertion->rule[0] - $assertion->rule[0] - $assertion->rule[0] - $assertion->rule[0] - $assertion->rule[0] - $count_expr->getArgs()[0] - $count_expr->getArgs()[0] - $count_expr->getArgs()[0] - $count_expr->getArgs()[0] - $count_expr->getArgs()[0] - $counted_expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[1] - $expr->getArgs()[1] - $get_debug_type_expr->getArgs()[0] - $get_debug_type_expr->getArgs()[0] - $getclass_expr->getArgs()[0] - $gettype_expr->getArgs()[0] - $gettype_expr->getArgs()[0] + rule[0]]]> + rule[0]]]> + rule[0]]]> + rule[0]]]> + rule[0]]]> + rule[0]]]> + rule[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[1]]]> + getArgs()[1]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> + getArgs()[0]]]> @@ -129,7 +129,7 @@ $method_name $parts[1] - explode('::', $cased_method_id)[1] + @@ -143,7 +143,7 @@ - $stmt->getArgs()[0] + getArgs()[0]]]> $parts[1] @@ -156,12 +156,12 @@ - $result->invalid_method_call_types[0] - $result->non_existent_class_method_ids[0] - $result->non_existent_class_method_ids[0] - $result->non_existent_interface_method_ids[0] - $result->non_existent_interface_method_ids[0] - $result->non_existent_magic_method_ids[0] + invalid_method_call_types[0]]]> + non_existent_class_method_ids[0]]]> + non_existent_class_method_ids[0]]]> + non_existent_interface_method_ids[0]]]> + non_existent_interface_method_ids[0]]]> + non_existent_magic_method_ids[0]]]> @@ -171,8 +171,8 @@ - $callable_arg->items[0] - $callable_arg->items[1] + items[0]]]> + items[1]]]> @@ -205,7 +205,7 @@ - $atomic_return_type->type_params[2] + type_params[2]]]> @@ -225,7 +225,7 @@ - $stmt->expr->getArgs()[0] + expr->getArgs()[0]]]> $check_type_string @@ -233,7 +233,7 @@ - $options['tcp'] ?? null + @@ -283,11 +283,11 @@ - $a->props[0] - $a->stmts[0] + props[0]]]> + stmts[0]]]> $a_stmt_comments[0] - $b->props[0] - $b->stmts[0] + props[0]]]> + stmts[0]]]> $b_stmt_comments[0] @@ -304,14 +304,14 @@ - $stmt->props[0] + props[0]]]> - $type < 1 - $type < 1 || $type > 4 - $type > 4 + + 4]]> + 4]]> @@ -347,7 +347,7 @@ $match[0] $match[1] $match[2] - $node->stmts[0] + stmts[0]]]> $replacement_stmts[0] $replacement_stmts[0] $replacement_stmts[0] @@ -357,8 +357,8 @@ $doc_line_parts[1] $matches[0] - $method_tree->children[0] - $method_tree->children[1] + children[0]]]> + children[1]]]> @@ -369,8 +369,8 @@ - $node->getArgs()[0] - $node->getArgs()[1] + getArgs()[0]]]> + getArgs()[1]]]> @@ -378,7 +378,7 @@ $since_parts[1] - count($line_parts) > 0 + 0]]> @@ -391,7 +391,7 @@ - $stmt->stmts[0] + stmts[0]]]> @@ -419,8 +419,8 @@ isContainedBy - $array->properties[0] - $array->properties[0] + properties[0]]]> + properties[0]]]> @@ -433,7 +433,7 @@ - $array_atomic_type->properties[0] + properties[0]]]> $properties[0] $properties[0] $properties[0] @@ -447,13 +447,13 @@ - $combination->array_type_params[1] - $combination->array_type_params[1] - $combination->array_type_params[1] - $combination->array_type_params[1] - $combination->array_type_params[1] - $combination->array_type_params[1] - $combination->array_type_params[1] + array_type_params[1]]]> + array_type_params[1]]]> + array_type_params[1]]]> + array_type_params[1]]]> + array_type_params[1]]]> + array_type_params[1]]]> + array_type_params[1]]]> @@ -466,8 +466,8 @@ $const_name $const_name $intersection_types[0] - $parse_tree->children[0] - $parse_tree->condition->children[0] + children[0]]]> + condition->children[0]]]> array_keys($offset_template_data)[0] array_keys($template_type_map[$array_param_name])[0] array_keys($template_type_map[$class_name])[0] @@ -485,7 +485,7 @@ - $codebase->config->shepherd_host + config->shepherd_host]]> @@ -543,7 +543,7 @@ replace - $this->type_params[1] + type_params[1]]]> @@ -569,7 +569,7 @@ replace - $cloned->value_param + value_param]]> @@ -585,8 +585,8 @@ TList - new TList($this->getGenericValueType()) - new TNonEmptyList($this->getGenericValueType()) + getGenericValueType())]]> + getGenericValueType())]]> combine @@ -604,12 +604,12 @@ replace - $key_type->possibly_undefined + possibly_undefined]]> - $this->properties[0] - $this->properties[0] - $this->properties[0] + properties[0]]]> + properties[0]]]> + properties[0]]]> getList @@ -621,7 +621,7 @@ replace - $cloned->type_param + type_param]]> @@ -697,7 +697,7 @@ TArray|TKeyedArray|TClassStringMap - $this->types['array'] + types['array']]]> allFloatLiterals @@ -708,7 +708,7 @@ - $subNodes['expr'] + diff --git a/src/Psalm/ErrorBaseline.php b/src/Psalm/ErrorBaseline.php index 1206a518c..135a2a984 100644 --- a/src/Psalm/ErrorBaseline.php +++ b/src/Psalm/ErrorBaseline.php @@ -266,7 +266,12 @@ final class ErrorBaseline foreach ($existingIssueType['s'] as $selection) { $codeNode = $baselineDoc->createElement('code'); - $codeNode->textContent = trim($selection); + $textContent = trim($selection); + if ($textContent !== \htmlspecialchars($textContent)) { + $codeNode->append($baselineDoc->createCDATASection($textContent)); + } else { + $codeNode->textContent = trim($textContent); + } $issueNode->appendChild($codeNode); } $fileNode->appendChild($issueNode); From 375cdeb63620420bcf7bb4a4727be24dde43cb79 Mon Sep 17 00:00:00 2001 From: Jack Worman Date: Wed, 25 Jan 2023 10:57:28 -0500 Subject: [PATCH 7/7] cdata in baseline --- src/Psalm/ErrorBaseline.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Psalm/ErrorBaseline.php b/src/Psalm/ErrorBaseline.php index 135a2a984..227132769 100644 --- a/src/Psalm/ErrorBaseline.php +++ b/src/Psalm/ErrorBaseline.php @@ -16,6 +16,7 @@ use function array_merge; use function array_reduce; use function array_values; use function get_loaded_extensions; +use function htmlspecialchars; use function implode; use function ksort; use function min; @@ -267,7 +268,7 @@ final class ErrorBaseline foreach ($existingIssueType['s'] as $selection) { $codeNode = $baselineDoc->createElement('code'); $textContent = trim($selection); - if ($textContent !== \htmlspecialchars($textContent)) { + if ($textContent !== htmlspecialchars($textContent)) { $codeNode->append($baselineDoc->createCDATASection($textContent)); } else { $codeNode->textContent = trim($textContent);