Skip to content

Commit acbb53e

Browse files
committed
[FEATURE] Report the line of unresolved references
Warnings about references that cannot be resolved only name the file, which makes them hard to find in large documents. Links are resolved long after parsing, when the line is no longer known, and text roles only receive the DocumentParserContext, which has no line. Link nodes now carry their source location, like DirectiveNode does since 00e6fab, and the reference resolvers include it in their warnings. It is set centrally by the InlineParser for every link an inline rule or text role returns, so rules and text roles added by extensions are covered without changes, and extensions like render-guides can use it for their own link warnings. Signed-off-by: linawolf Assisted-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent 33fd534 commit acbb53e

13 files changed

Lines changed: 355 additions & 13 deletions

File tree

‎packages/guides-restructured-text/src/RestructuredText/Directives/ImageDirective.php‎

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@
1515

1616
use phpDocumentor\Guides\Compiler\CompilerContextInterface;
1717
use phpDocumentor\Guides\Nodes\ImageNode;
18+
use phpDocumentor\Guides\Nodes\Inline\AbstractLinkInlineNode;
1819
use phpDocumentor\Guides\Nodes\Inline\DocReferenceNode;
1920
use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode;
20-
use phpDocumentor\Guides\Nodes\Inline\LinkInlineNode;
2121
use phpDocumentor\Guides\Nodes\Inline\ReferenceNode;
2222
use phpDocumentor\Guides\Nodes\Node;
2323
use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface;
@@ -70,17 +70,15 @@ public function createNode(DirectiveNode $directiveNode, CompilerContextInterfac
7070
),
7171
);
7272
if ($directive->hasOption('target')) {
73-
$node->setTarget(
74-
$this->resolveLinkTarget(
75-
$directive->getOptionString('target'),
76-
),
77-
);
73+
$target = $this->resolveLinkTarget($directive->getOptionString('target'));
74+
$target->setLoggerInformation($directiveNode->getSourceLocation()->toLoggerInformation());
75+
$node->setTarget($target);
7876
}
7977

8078
return $node;
8179
}
8280

83-
private function resolveLinkTarget(string $targetReference): LinkInlineNode
81+
private function resolveLinkTarget(string $targetReference): AbstractLinkInlineNode
8482
{
8583
if (filter_var($targetReference, FILTER_VALIDATE_EMAIL)) {
8684
return new HyperLinkNode([], $targetReference);

‎packages/guides-restructured-text/src/RestructuredText/Parser/InlineParser.php‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@
1414
namespace phpDocumentor\Guides\RestructuredText\Parser;
1515

1616
use Exception;
17+
use phpDocumentor\Guides\Nodes\Inline\AbstractLinkInlineNode;
1718
use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode;
1819
use phpDocumentor\Guides\Nodes\InlineCompoundNode;
1920
use phpDocumentor\Guides\RestructuredText\Parser\Productions\InlineRules\CachableInlineRule;
2021
use phpDocumentor\Guides\RestructuredText\Parser\Productions\InlineRules\InlineRule;
2122

2223
use function array_filter;
2324
use function array_key_exists;
25+
use function substr_count;
2426
use function usort;
2527

2628
/** @internal */
@@ -48,15 +50,21 @@ public function __construct(
4850
}
4951
}
5052

51-
public function parse(string $content, BlockContext $blockContext): InlineCompoundNode
53+
/**
54+
* @param int|null $firstLineNumber Line in the source file $content starts on, defaults to the current line of
55+
* $blockContext
56+
*/
57+
public function parse(string $content, BlockContext $blockContext, int|null $firstLineNumber = null): InlineCompoundNode
5258
{
59+
$firstLineNumber ??= $blockContext->getCurrentLineNumber();
5360
$lexer = new InlineLexer($this->disableLegacyTilde);
5461
$lexer->setInput($content);
5562
$lexer->moveNext();
5663
$lexer->moveNext();
5764
$nodes = [];
5865
$previous = null;
5966
while ($lexer->token !== null) {
67+
$position = $lexer->token->position;
6068
foreach ($this->rules as $inlineRule) {
6169
$node = null;
6270
if (array_key_exists($lexer->token->type ?? -1, $this->cache)) {
@@ -69,6 +77,12 @@ public function parse(string $content, BlockContext $blockContext): InlineCompou
6977
continue;
7078
}
7179

80+
if ($node instanceof AbstractLinkInlineNode && $node->getLoggerInformation() === []) {
81+
$loggerInformation = $blockContext->getDocumentParserContext()->getLoggerInformation();
82+
$loggerInformation['currentLineNumber'] = $firstLineNumber + substr_count($content, "\n", 0, $position);
83+
$node->setLoggerInformation($loggerInformation);
84+
}
85+
7286
if ($previous instanceof PlainTextInlineNode && $node instanceof PlainTextInlineNode) {
7387
$previous->append($node);
7488
} else {

‎packages/guides-restructured-text/src/RestructuredText/Parser/Productions/InlineMarkupRule.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ public function applies(BlockContext $blockContext): bool
6666
public function apply(BlockContext $blockContext, CompoundNode|null $on = null): CompoundNode|InlineCompoundNode
6767
{
6868
$documentIterator = $blockContext->getDocumentIterator();
69+
$firstLineNumber = $blockContext->getCurrentLineNumber();
6970
$buffer = $this->collectContent($documentIterator);
7071

71-
$node = $this->inlineTokenParser->parse($buffer->getLinesString(), $blockContext);
72+
$node = $this->inlineTokenParser->parse($buffer->getLinesString(), $blockContext, $firstLineNumber);
7273

7374
if ($on !== null) {
7475
$on->setValue([$node]);

‎packages/guides-restructured-text/src/RestructuredText/Parser/Productions/TitleRule.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public function apply(BlockContext $blockContext, CompoundNode|null $on = null):
6666
$title = trim($documentIterator->current()); // Title with over and underlines may be indented
6767
}
6868

69+
$titleLineNumber = $blockContext->getCurrentLineNumber();
6970
$underlineLetter = $this->nextLineIsAnUnderline($documentIterator->current(), $documentIterator->getNextLine());
7071
if ($underlineLetter !== '') {
7172
if (($overlineLetter === '' || $overlineLetter === $underlineLetter)) {
@@ -82,7 +83,7 @@ public function apply(BlockContext $blockContext, CompoundNode|null $on = null):
8283
$level = $blockContext->getDocumentParserContext()->getLevel($overlineLetter, $underlineLetter);
8384

8485
return new TitleNode(
85-
$this->inlineTokenParser->parse($title, $blockContext),
86+
$this->inlineTokenParser->parse($title, $blockContext, $titleLineNumber),
8687
$level,
8788
(new AsciiSlugger())->slug($title)->lower()->toString(),
8889
);

‎packages/guides-restructured-text/tests/unit/Parser/InlineTokenParserTest.php‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace phpDocumentor\Guides\RestructuredText\Parser;
1515

16+
use phpDocumentor\Guides\Nodes\Inline\AbstractLinkInlineNode;
1617
use phpDocumentor\Guides\Nodes\Inline\CitationInlineNode;
1718
use phpDocumentor\Guides\Nodes\Inline\DocReferenceNode;
1819
use phpDocumentor\Guides\Nodes\Inline\EmphasisInlineNode;
@@ -95,9 +96,38 @@ public function setUp(): void
9596
public function testString(string $content, InlineCompoundNode $expected): void
9697
{
9798
$result = $this->inlineTokenParser->parse($content, new BlockContext($this->documentParserContext, ''));
99+
// Link nodes also carry their source location, covered by testLinkLineNumbers()
100+
foreach ($result->getChildren() as $child) {
101+
if (!($child instanceof AbstractLinkInlineNode)) {
102+
continue;
103+
}
104+
105+
$child->setLoggerInformation([]);
106+
}
107+
98108
self::assertEquals($expected, $result);
99109
}
100110

111+
public function testLinkLineNumbers(): void
112+
{
113+
$result = $this->inlineTokenParser->parse(
114+
"first line\nsecond :doc:`some/document` and\nthird myref_",
115+
new BlockContext($this->documentParserContext, ''),
116+
5,
117+
);
118+
119+
$lineNumbers = [];
120+
foreach ($result->getChildren() as $child) {
121+
if (!($child instanceof AbstractLinkInlineNode)) {
122+
continue;
123+
}
124+
125+
$lineNumbers[$child->getTargetReference()] = $child->getLoggerInformation()['currentLineNumber'] ?? null;
126+
}
127+
128+
self::assertSame(['some/document' => 6, 'myref' => 7], $lineNumbers);
129+
}
130+
101131
/** @return array<string, array<string | InlineCompoundNode>> */
102132
public static function inlineNodeProvider(): array
103133
{

‎packages/guides/src/Nodes/Inline/AbstractLinkInlineNode.php‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ abstract class AbstractLinkInlineNode extends InlineCompoundNode implements Link
2626

2727
private string $url = '';
2828

29+
/** @var array<string, int|string> */
30+
private array $loggerInformation = [];
31+
2932
/** @param InlineNodeInterface[] $children */
3033
public function __construct(
3134
private readonly string $type,
@@ -78,6 +81,23 @@ public function getDebugInformation(): array
7881
];
7982
}
8083

84+
/**
85+
* Source location (file, line) of the link, for warnings logged after
86+
* parsing, like a reference that cannot be resolved.
87+
*
88+
* @param array<string, int|string> $loggerInformation
89+
*/
90+
public function setLoggerInformation(array $loggerInformation): void
91+
{
92+
$this->loggerInformation = $loggerInformation;
93+
}
94+
95+
/** @return array<string, int|string> */
96+
public function getLoggerInformation(): array
97+
{
98+
return $this->loggerInformation;
99+
}
100+
81101
public function getType(): string
82102
{
83103
return $this->type;

‎packages/guides/src/ReferenceResolvers/ImageReferenceResolverPreRender.php‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
use phpDocumentor\Guides\NodeRenderers\PreRenderers\PreNodeRenderer;
1717
use phpDocumentor\Guides\Nodes\ImageNode;
18+
use phpDocumentor\Guides\Nodes\Inline\AbstractLinkInlineNode;
1819
use phpDocumentor\Guides\Nodes\Node;
1920
use phpDocumentor\Guides\RenderContext;
2021
use Psr\Log\LoggerInterface;
@@ -53,7 +54,11 @@ public function execute(Node $node, RenderContext $renderContext): Node
5354
$referenceLinkNode->getTargetReference(),
5455
$renderContext->getCurrentFileName(),
5556
),
56-
array_merge($renderContext->getLoggerInformation(), $messages->getLastWarning()?->getDebugInfo() ?? []),
57+
array_merge(
58+
$referenceLinkNode instanceof AbstractLinkInlineNode ? $referenceLinkNode->getLoggerInformation() : [],
59+
$renderContext->getLoggerInformation(),
60+
$messages->getLastWarning()?->getDebugInfo() ?? [],
61+
),
5762
);
5863
}
5964

‎packages/guides/src/ReferenceResolvers/ReferenceResolverPreRender.php‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
namespace phpDocumentor\Guides\ReferenceResolvers;
1515

1616
use phpDocumentor\Guides\NodeRenderers\PreRenderers\PreNodeRenderer;
17+
use phpDocumentor\Guides\Nodes\Inline\AbstractLinkInlineNode;
1718
use phpDocumentor\Guides\Nodes\Inline\LinkInlineNode;
1819
use phpDocumentor\Guides\Nodes\Node;
1920
use phpDocumentor\Guides\RenderContext;
@@ -48,7 +49,11 @@ public function execute(Node $node, RenderContext $renderContext): Node
4849
$node->getTargetReference(),
4950
$renderContext->getCurrentFileName(),
5051
),
51-
array_merge($renderContext->getLoggerInformation(), $messages->getLastWarning()?->getDebugInfo() ?? []),
52+
array_merge(
53+
$node instanceof AbstractLinkInlineNode ? $node->getLoggerInformation() : [],
54+
$renderContext->getLoggerInformation(),
55+
$messages->getLastWarning()?->getDebugInfo() ?? [],
56+
),
5257
);
5358
}
5459

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
app.WARNING: Inventory with key unknowninventory not found. {"rst-file":"Index","type":"doc","targetReference":"Index","value":"TYPO3 documentation","interlinkDomain":"unknowninventory"} []
1+
app.WARNING: Inventory with key unknowninventory not found. {"rst-file":"Index","currentLineNumber":7,"type":"doc","targetReference":"Index","value":"TYPO3 documentation","interlinkDomain":"unknowninventory"} []
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
app.WARNING: Reference ref-line-4 could not be resolved in index {"rst-file":"index","currentLineNumber":4} []
2+
app.WARNING: Reference ref-line-6 could not be resolved in index {"rst-file":"index","currentLineNumber":6} []
3+
app.WARNING: Document with name "doc-line-7" not found, required in file "index". {"rst-file":"index","currentLineNumber":7} []
4+
app.WARNING: Reference hyperlink-line-8 could not be resolved in index {"rst-file":"index","currentLineNumber":8} []
5+
app.WARNING: Reference ref-line-11 could not be resolved in index {"rst-file":"index","currentLineNumber":11} []
6+
app.WARNING: Reference ref-line-14 could not be resolved in index {"rst-file":"index","currentLineNumber":14} []
7+
app.WARNING: Reference ref-line-16 could not be resolved in index {"rst-file":"index","currentLineNumber":16} []
8+
app.WARNING: Reference ref-line-18 could not be resolved in index {"rst-file":"index","currentLineNumber":18} []
9+
app.WARNING: Reference ref-line-20 could not be resolved in index {"rst-file":"index","currentLineNumber":20} []
10+
app.WARNING: Reference ref-line-21 could not be resolved in index {"rst-file":"index","currentLineNumber":21} []
11+
app.WARNING: Reference ref-line-23 could not be resolved in index {"rst-file":"index","currentLineNumber":23} []
12+
app.WARNING: Reference ref-line-24 could not be resolved in index {"rst-file":"index","currentLineNumber":24} []
13+
app.WARNING: Reference ref-line-26 could not be resolved in index {"rst-file":"index","currentLineNumber":26} []
14+
app.WARNING: Reference ref-line-28 could not be resolved in index {"rst-file":"index","currentLineNumber":28} []
15+
app.WARNING: Reference ref-line-30 could not be resolved in index {"rst-file":"index","currentLineNumber":30} []
16+
app.WARNING: Reference ref-line-31 could not be resolved in index {"rst-file":"index","currentLineNumber":31} []
17+
app.WARNING: Reference ref-line-33 could not be resolved in index {"rst-file":"index","currentLineNumber":33} []
18+
app.WARNING: Reference ref-line-36 could not be resolved in index {"rst-file":"index","currentLineNumber":36} []
19+
app.WARNING: Reference ref-line-37 could not be resolved in index {"rst-file":"index","currentLineNumber":37} []
20+
app.WARNING: Target image-target-line-40 of image could not be resolved in index {"rst-file":"index","currentLineNumber":40} []
21+
app.WARNING: Reference ref-line-48 could not be resolved in index {"rst-file":"index","currentLineNumber":48} []
22+
app.WARNING: Reference anonymous-line-49 could not be resolved in index {"rst-file":"index","currentLineNumber":49} []
23+
app.WARNING: Reference ref-line-51 could not be resolved in index {"rst-file":"index","currentLineNumber":51} []
24+
app.WARNING: Reference ref-line-58 could not be resolved in index {"rst-file":"index","currentLineNumber":58} []

0 commit comments

Comments
 (0)