Skip to content

Commit 4140e78

Browse files
committed
Fix @throws line number to use actual tag line
The line number assigned to @throws dependencies was incorrectly set to the function's start line. This fix computes the actual file line of each @throws tag using PHPStan's startLine attribute (line relative to the docblock) combined with the docblock's file start line. Added assertions to verify correct line numbers for all @throws test cases. https://claude.ai/code/session_01MV9occCkrjhX1JmKUsaYZA
1 parent fa33a58 commit 4140e78

File tree

4 files changed

+47
-12
lines changed

4 files changed

+47
-12
lines changed

src/Analyzer/Docblock.php

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,30 @@ public function getThrowTagsTypes(): array
5555
return array_filter($throwTypes);
5656
}
5757

58+
/**
59+
* @return array<array{type: string, line: int}>
60+
*/
61+
public function getThrowTagsTypesWithLines(): array
62+
{
63+
$result = [];
64+
65+
foreach ($this->phpDocNode->getTags() as $tag) {
66+
if (!($tag->value instanceof ThrowsTagValueNode)) {
67+
continue;
68+
}
69+
70+
$type = $this->getType($tag->value->type);
71+
72+
if (null === $type) {
73+
continue;
74+
}
75+
76+
$result[] = ['type' => $type, 'line' => $tag->getAttribute('startLine') ?? 1];
77+
}
78+
79+
return $result;
80+
}
81+
5882
public function getVarTagTypes(): array
5983
{
6084
$varTypes = array_map(

src/Analyzer/DocblockTypesResolver.php

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,23 +181,27 @@ private function resolveReturnValueType(Node $node, Docblock $docblock): void
181181
*/
182182
private function resolveThrowsValueType(Node $node, Docblock $docblock): void
183183
{
184-
// extract throw types from throw tag
185-
$throwValues = $docblock->getThrowTagsTypes();
184+
// extract throw types from throw tag, with their docblock-relative line numbers
185+
$throwValues = $docblock->getThrowTagsTypesWithLines();
186186

187187
if (empty($throwValues)) {
188188
return;
189189
}
190190

191+
$docComment = $node->getDocComment();
192+
$docblockStartLine = null !== $docComment ? $docComment->getStartLine() : $node->getStartLine();
193+
191194
$throwsTypesResolved = [];
192195

193-
foreach ($throwValues as $throwValue) {
196+
foreach ($throwValues as ['type' => $throwValue, 'line' => $tagDocblockLine]) {
194197
if (str_starts_with($throwValue, '\\')) {
195198
$name = new FullyQualified(substr($throwValue, 1));
196199
} else {
197200
$name = $this->resolveName(new Name($throwValue), Stmt\Use_::TYPE_NORMAL);
198201
}
199202

200-
$name->setAttribute('startLine', $node->getStartLine());
203+
// compute the actual file line: docblock start + tag's line within docblock - 1
204+
$name->setAttribute('startLine', $docblockStartLine + $tagDocblockLine - 1);
201205

202206
$throwsTypesResolved[] = $name;
203207
}

tests/Unit/Analyzer/DocblockTypesResolverTest.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,11 @@ public function myMethod2(array $aParam, array $users): array
9090
self::assertEquals('Application\Model\Product', $dep[4]->getFQCN()->toString());
9191
self::assertEquals('Domain\Foo\MyOtherClass', $dep[5]->getFQCN()->toString());
9292
self::assertEquals('Exception', $dep[6]->getFQCN()->toString());
93+
self::assertEquals(42, $dep[6]->getLine());
9394
self::assertEquals('Domain\Foo\FooException', $dep[7]->getFQCN()->toString());
95+
self::assertEquals(43, $dep[7]->getLine());
9496
self::assertEquals('Domain\Foo\BarException', $dep[8]->getFQCN()->toString());
97+
self::assertEquals(44, $dep[8]->getLine());
9598

9699
self::assertEquals('Application\Model\User', $dep[9]->getFQCN()->toString());
97100
self::assertEquals(46, $dep[9]->getLine());

tests/Unit/Analyzer/FileParser/CanParseDocblocksTest.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -536,10 +536,12 @@ public function method2()
536536
// Should have 3 dependencies from @throws: FooException, BarException, Exception
537537
self::assertCount(3, $dependencies);
538538

539-
$fqcns = array_map(static fn ($dep) => $dep->getFQCN()->toString(), $dependencies);
540-
self::assertContains('Domain\FooException', $fqcns);
541-
self::assertContains('Domain\BarException', $fqcns);
542-
self::assertContains('Exception', $fqcns);
539+
self::assertEquals('Domain\FooException', $dependencies[0]->getFQCN()->toString());
540+
self::assertEquals(11, $dependencies[0]->getLine());
541+
self::assertEquals('Domain\BarException', $dependencies[1]->getFQCN()->toString());
542+
self::assertEquals(12, $dependencies[1]->getLine());
543+
self::assertEquals('Exception', $dependencies[2]->getFQCN()->toString());
544+
self::assertEquals(19, $dependencies[2]->getLine());
543545
}
544546

545547
public function test_it_collects_throws_tag_with_fully_qualified_names(): void
@@ -573,9 +575,11 @@ public function doSomething()
573575
// Should have 3 dependencies from @throws
574576
self::assertCount(3, $dependencies);
575577

576-
$fqcns = array_map(static fn ($dep) => $dep->getFQCN()->toString(), $dependencies);
577-
self::assertContains('Exception', $fqcns);
578-
self::assertContains('Domain\FooException', $fqcns);
579-
self::assertContains('App\Services\BarException', $fqcns);
578+
self::assertEquals('Exception', $dependencies[0]->getFQCN()->toString());
579+
self::assertEquals(8, $dependencies[0]->getLine());
580+
self::assertEquals('Domain\FooException', $dependencies[1]->getFQCN()->toString());
581+
self::assertEquals(9, $dependencies[1]->getLine());
582+
self::assertEquals('App\Services\BarException', $dependencies[2]->getFQCN()->toString());
583+
self::assertEquals(10, $dependencies[2]->getLine());
580584
}
581585
}

0 commit comments

Comments
 (0)