diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index 0589a430a88..34433a6cd1e 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -23,6 +23,7 @@ use function array_key_first; use function array_shift; use function count; +use function explode; use function implode; use function in_array; use function preg_match; @@ -182,8 +183,7 @@ public static function parse( } } - if (isset($parsed_docblock->tags['psalm-yield']) - ) { + if (isset($parsed_docblock->tags['psalm-yield'])) { $yield = reset($parsed_docblock->tags['psalm-yield']); $info->yield = trim(preg_replace('@^[ \t]*\*@m', '', $yield)); @@ -437,7 +437,9 @@ public static function parse( /** @var Doc */ $node_doc_comment = $node->getDocComment(); - $statements[0]->stmts[0]->setAttribute('startLine', $node_doc_comment->getStartLine()); + $method_offset = self::getMethodOffset($comment, $method_entry); + + $statements[0]->stmts[0]->setAttribute('startLine', $node_doc_comment->getStartLine() + $method_offset); $statements[0]->stmts[0]->setAttribute('startFilePos', $node_doc_comment->getStartFilePos()); $statements[0]->stmts[0]->setAttribute('endFilePos', $node->getAttribute('startFilePos')); @@ -545,4 +547,19 @@ protected static function addMagicPropertyToInfo( } } } + + private static function getMethodOffset(Doc $comment, string $method_entry): int + { + $lines = explode("\n", $comment->getText()); + $method_offset = 0; + + foreach ($lines as $i => $line) { + if (strpos($line, $method_entry) !== false) { + $method_offset = $i; + break; + } + } + + return $method_offset; + } } diff --git a/tests/AnnotationTest.php b/tests/AnnotationTest.php index 98a2ec81d5c..3cde100031a 100644 --- a/tests/AnnotationTest.php +++ b/tests/AnnotationTest.php @@ -164,6 +164,77 @@ function takesArrayIteratorOfString(ArrayIterator $i): void { $this->analyzeFile('somefile.php', new Context()); } + public function testLessSpecificImplementedReturnTypeWithDocblockOnMultipleLines(): void + { + $this->expectException(CodeException::class); + $this->expectExceptionMessage('LessSpecificImplementedReturnType - somefile.php:5:'); + + $this->addFile( + 'somefile.php', + 'analyzeFile('somefile.php', new Context()); + } + + public function testLessSpecificImplementedReturnTypeWithDocblockOnMultipleLinesWithMultipleClasses(): void + { + $this->expectException(CodeException::class); + $this->expectExceptionMessage('LessSpecificImplementedReturnType - somefile.php:15:'); + + $this->addFile( + 'somefile.php', + 'analyzeFile('somefile.php', new Context()); + } + + public function testLessSpecificImplementedReturnTypeWithDescription(): void + { + $this->expectException(CodeException::class); + $this->expectExceptionMessage('LessSpecificImplementedReturnType - somefile.php:7:'); + + $this->addFile( + 'somefile.php', + 'analyzeFile('somefile.php', new Context()); + } + public function testPhpStormGenericsNoTypehint(): void { $this->expectException(CodeException::class);