From 6b3380b05fcfad2ea146de993b90f269789eb235 Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Tue, 6 Feb 2024 15:20:02 +0000 Subject: [PATCH 1/2] Regression and fail tests List fail tests: ``` 1) testMethodAnnotation with data set "static (string|int)[] getArray()" Undefined array key 0 /workspaces/psalm/tests/ClassLikeDocblockParserTest.php:201 2) testMethodAnnotation with data set "static (callable() : string) getCallable()" Undefined array key 0 /workspaces/psalm/tests/ClassLikeDocblockParserTest.php:201 ``` --- tests/ClassLikeDocblockParserTest.php | 184 ++++++++++++++++++++++++++ 1 file changed, 184 insertions(+) diff --git a/tests/ClassLikeDocblockParserTest.php b/tests/ClassLikeDocblockParserTest.php index 96a0f6911f7..4fd36cf7ae8 100644 --- a/tests/ClassLikeDocblockParserTest.php +++ b/tests/ClassLikeDocblockParserTest.php @@ -7,6 +7,8 @@ use Psalm\Aliases; use Psalm\Internal\PhpVisitor\Reflector\ClassLikeDocblockParser; +use function array_values; + class ClassLikeDocblockParserTest extends TestCase { public function testDocblockDescription(): void @@ -35,4 +37,186 @@ public function testPreferPsalmPrefixedAnnotationsOverPhpstanOnes(): void $class_docblock = ClassLikeDocblockParser::parse($node, $php_parser_doc, new Aliases()); $this->assertSame([['T', 'of', 'string', true, 33]], $class_docblock->templates); } + + /** + * @return iterable + */ + public function providerMethodAnnotation(): iterable + { + $data = [ + 'foo()' => [ + 'name' => 'foo', + 'returnType' => '', + 'is_static' => false, + 'params' => [], + ], + 'foo($a)' => [ + 'name' => 'foo', + 'returnType' => '', + 'is_static' => false, + 'params' => [ + 'a' => ['type' => ''], + ], + ], + 'string foo()' => [ + 'name' => 'foo', + 'returnType' => 'string', + 'is_static' => false, + 'params' => [], + ], + 'static string foo()' => [ + 'name' => 'foo', + 'returnType' => 'string', + 'is_static' => true, + 'params' => [], + ], + 'string foo(string $a, int $b)' => [ + 'name' => 'foo', + 'returnType' => 'string', + 'is_static' => false, + 'params' => [ + 'a' => ['type' => 'string'], + 'b' => ['type' => 'int'], + ], + ], + 'static string foo(string $a, int $b)' => [ + 'name' => 'foo', + 'returnType' => 'string', + 'is_static' => true, + 'params' => [ + 'a' => ['type' => 'string'], + 'b' => ['type' => 'int'], + ], + ], + 'static foo()' => [ + 'name' => 'foo', + 'returnType' => 'static', + 'is_static' => false, + 'params' => [], + ], + 'static static foo()' => [ + 'name' => 'foo', + 'returnType' => 'static', + 'is_static' => true, + 'params' => [], + ], + 'static foo(string $z)' => [ + 'name' => 'foo', + 'returnType' => 'static', + 'is_static' => false, + 'params' => [ + 'z' => ['type' => 'string'], + ], + ], + 'static static foo(string $z)' => [ + 'name' => 'foo', + 'returnType' => 'static', + 'is_static' => true, + 'params' => [ + 'z' => ['type' => 'string'], + ], + ], + 'self foo()' => [ + 'name' => 'foo', + 'returnType' => 'MyClass', + 'is_static' => false, + 'params' => [], + ], + 'static self foo()' => [ + 'name' => 'foo', + 'returnType' => 'MyClass', + 'is_static' => true, + 'params' => [], + ], + 'self foo(string $z)' => [ + 'name' => 'foo', + 'returnType' => 'MyClass', + 'is_static' => false, + 'params' => [ + 'z' => ['type' => 'string'], + ], + ], + 'static self foo(string $z)' => [ + 'name' => 'foo', + 'returnType' => 'MyClass', + 'is_static' => true, + 'params' => [ + 'z' => ['type' => 'string'], + ], + ], + '(string|int)[] getArray()' => [ + 'name' => 'getArray', + 'returnType' => 'array', + 'is_static' => false, + 'params' => [], + ], + 'static (string|int)[] getArray()' => [ + 'name' => 'getArray', + 'returnType' => 'array', + 'is_static' => true, + 'params' => [], + ], + '(callable() : string) getCallable()' => [ + 'name' => 'getCallable', + 'returnType' => 'callable():string', + 'is_static' => false, + 'params' => [], + ], + 'static (callable() : string) getCallable()' => [ + 'name' => 'getCallable', + 'returnType' => 'callable():string', + 'is_static' => true, + 'params' => [], + ], + ]; + + $res = []; + foreach ($data as $key => $item) { + $res[$key] = [ + 'annotation' => $key, + 'expected' => $item, + ]; + } + + return $res; + } + + /** + * @dataProvider providerMethodAnnotation + */ + public function testMethodAnnotation(string $annotation, array $expected): void + { + $full_content = <<addFile('somefile.php', $full_content); + + $codebase = $this->project_analyzer->getCodebase(); + $codebase->scanFiles(); + + $class_storage = $codebase->classlike_storage_provider->get('MyClass'); + $methods = $expected['is_static'] + ? $class_storage->pseudo_static_methods + : $class_storage->pseudo_methods; + $method = array_values($methods)[0]; + + $actual = [ + 'name' => $method->cased_name, + 'returnType' => (string) $method->return_type, + 'is_static' => $method->is_static, + 'params' => [], + ]; + foreach ($method->params as $param) { + $actual['params'][$param->name] = [ + 'type' => (string) $param->type, + ]; + } + + $this->assertEquals($expected, $actual); + } } From afaaa3d6dfb4fca8ffb13b3e7a2d49b8a797a08f Mon Sep 17 00:00:00 2001 From: Ivan Sidorov Date: Tue, 6 Feb 2024 16:09:32 +0000 Subject: [PATCH 2/2] Fix parsing magic method annotations Fix parsing for code: ``` /** * @method static (string|int)[] getArray() * @method static (callable() : string) getCallable() */ class MyClass {} ``` Resolved tests: ``` 1) testMethodAnnotation with data set "static (string|int)[] getArray()" 2) testMethodAnnotation with data set "static (callable() : string) getCallable()" ``` --- .../Reflector/ClassLikeDocblockParser.php | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index 1d48be61220..2a6922627c2 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -322,14 +322,19 @@ public static function parse( $has_return = false; - if (!preg_match('/^([a-z_A-Z][a-z_0-9A-Z]+) *\(/', $method_entry, $matches)) { - $doc_line_parts = CommentAnalyzer::splitDocLine($method_entry); + $doc_line_parts = CommentAnalyzer::splitDocLine($method_entry); - if ($doc_line_parts[0] === 'static' && !strpos($doc_line_parts[1], '(')) { - $is_static = true; - array_shift($doc_line_parts); - } + if (count($doc_line_parts) > 2 + && $doc_line_parts[0] === 'static' + && !strpos($doc_line_parts[1], '(') + ) { + $is_static = true; + array_shift($doc_line_parts); + $method_entry = implode(' ', $doc_line_parts); + $doc_line_parts = CommentAnalyzer::splitDocLine($method_entry); + } + if (!preg_match('/^([a-z_A-Z][a-z_0-9A-Z]+) *\(/', $method_entry, $matches)) { if (count($doc_line_parts) > 1) { $docblock_lines[] = '@return ' . array_shift($doc_line_parts); $has_return = true;