Skip to content

Commit 1ef4ff3

Browse files
committed
Improve link validation warning messages
Resolves #2967
1 parent dbefdbd commit 1ef4ff3

File tree

7 files changed

+73
-88
lines changed

7 files changed

+73
-88
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ title: Changelog
1313
- Attempting to highlight a supported language which is not enabled is now a warning, not an error, #2956.
1414
- Improved compatibility with CommonMark's link parsing, #2959.
1515
- Classes, variables, and functions exported with `export { type X }` are now detected and converted as interfaces/type aliases, #2962.
16+
- Improved warning messaging for links to symbols which were resolved, but the symbols were not included in the documentation, #2967.
1617

1718
## v0.28.5 (2025-05-26)
1819

src/lib/internationalization/locales/en.cts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export = {
9191
inline_tag_not_closed: `Inline tag is not closed`,
9292

9393
// validation
94+
comment_for_0_links_to_1_not_included_in_docs_use_external_link_2:
95+
`The comment for {0} links to "{1}" which was resolved but is not included in the documentation. To fix this warning export it or add {2} to the externalSymbolLinkMappings option`,
9496
failed_to_resolve_link_to_0_in_comment_for_1: `Failed to resolve link to "{0}" in comment for {1}`,
9597
failed_to_resolve_link_to_0_in_comment_for_1_may_have_meant_2:
9698
`Failed to resolve link to "{0}" in comment for {1}. You may have wanted "{2}"`,

src/lib/validation/links.ts

Lines changed: 43 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { i18n, type Logger } from "#utils";
22
import {
33
type Comment,
44
type CommentDisplayPart,
5+
type InlineTagDisplayPart,
56
type ProjectReflection,
67
type Reflection,
78
ReflectionKind,
@@ -11,15 +12,15 @@ import {
1112
const linkTags = ["@link", "@linkcode", "@linkplain"];
1213

1314
function getBrokenPartLinks(parts: readonly CommentDisplayPart[]) {
14-
const links: string[] = [];
15+
const links: InlineTagDisplayPart[] = [];
1516

1617
for (const part of parts) {
1718
if (
1819
part.kind === "inline-tag" &&
1920
linkTags.includes(part.tag) &&
2021
(!part.target || part.target instanceof ReflectionSymbolId)
2122
) {
22-
links.push(part.text.trim());
23+
links.push(part);
2324
}
2425
}
2526

@@ -53,21 +54,22 @@ export function validateLinks(
5354
function checkReflection(reflection: Reflection, logger: Logger) {
5455
if (reflection.isProject() || reflection.isDeclaration()) {
5556
for (const broken of getBrokenPartLinks(reflection.readme || [])) {
57+
const linkText = broken.text.trim();
5658
// #2360, "@" is a future reserved character in TSDoc component paths
5759
// If a link starts with it, and doesn't include a module source indicator "!"
5860
// then the user probably is trying to link to a package containing "@" with an absolute link.
59-
if (broken.startsWith("@") && !broken.includes("!")) {
61+
if (linkText.startsWith("@") && !linkText.includes("!")) {
6062
logger.warn(
6163
i18n.failed_to_resolve_link_to_0_in_readme_for_1_may_have_meant_2(
62-
broken,
64+
linkText,
6365
reflection.getFriendlyFullName(),
64-
broken.replace(/[.#~]/, "!"),
66+
linkText.replace(/[.#~]/, "!"),
6567
),
6668
);
6769
} else {
6870
logger.warn(
6971
i18n.failed_to_resolve_link_to_0_in_readme_for_1(
70-
broken,
72+
linkText,
7173
reflection.getFriendlyFullName(),
7274
),
7375
);
@@ -77,21 +79,19 @@ function checkReflection(reflection: Reflection, logger: Logger) {
7779

7880
if (reflection.isDocument()) {
7981
for (const broken of getBrokenPartLinks(reflection.content)) {
80-
// #2360, "@" is a future reserved character in TSDoc component paths
81-
// If a link starts with it, and doesn't include a module source indicator "!"
82-
// then the user probably is trying to link to a package containing "@" with an absolute link.
83-
if (broken.startsWith("@") && !broken.includes("!")) {
82+
const linkText = broken.text.trim();
83+
if (linkText.startsWith("@") && !linkText.includes("!")) {
8484
logger.warn(
8585
i18n.failed_to_resolve_link_to_0_in_document_1_may_have_meant_2(
86-
broken,
86+
linkText,
8787
reflection.getFriendlyFullName(),
88-
broken.replace(/[.#~]/, "!"),
88+
linkText.replace(/[.#~]/, "!"),
8989
),
9090
);
9191
} else {
9292
logger.warn(
9393
i18n.failed_to_resolve_link_to_0_in_document_1(
94-
broken,
94+
linkText,
9595
reflection.getFriendlyFullName(),
9696
),
9797
);
@@ -100,25 +100,7 @@ function checkReflection(reflection: Reflection, logger: Logger) {
100100
}
101101

102102
for (const broken of getBrokenLinks(reflection.comment)) {
103-
// #2360, "@" is a future reserved character in TSDoc component paths
104-
// If a link starts with it, and doesn't include a module source indicator "!"
105-
// then the user probably is trying to link to a package containing "@" with an absolute link.
106-
if (broken.startsWith("@") && !broken.includes("!")) {
107-
logger.warn(
108-
i18n.failed_to_resolve_link_to_0_in_comment_for_1_may_have_meant_2(
109-
broken,
110-
reflection.getFriendlyFullName(),
111-
broken.replace(/[.#~]/, "!"),
112-
),
113-
);
114-
} else {
115-
logger.warn(
116-
i18n.failed_to_resolve_link_to_0_in_comment_for_1(
117-
broken,
118-
reflection.getFriendlyFullName(),
119-
),
120-
);
121-
}
103+
reportBrokenCommentLink(broken, reflection, logger);
122104
}
123105

124106
if (
@@ -132,22 +114,35 @@ function checkReflection(reflection: Reflection, logger: Logger) {
132114
getBrokenPartLinks,
133115
)
134116
) {
135-
if (broken.startsWith("@") && !broken.includes("!")) {
136-
logger.warn(
137-
i18n.failed_to_resolve_link_to_0_in_comment_for_1_may_have_meant_2(
138-
broken,
139-
reflection.getFriendlyFullName(),
140-
broken.replace(/[.#~]/, "!"),
141-
),
142-
);
143-
} else {
144-
logger.warn(
145-
i18n.failed_to_resolve_link_to_0_in_comment_for_1(
146-
broken,
147-
reflection.getFriendlyFullName(),
148-
),
149-
);
150-
}
117+
reportBrokenCommentLink(broken, reflection, logger);
151118
}
152119
}
153120
}
121+
122+
function reportBrokenCommentLink(broken: InlineTagDisplayPart, reflection: Reflection, logger: Logger) {
123+
const linkText = broken.text.trim();
124+
if (broken.target instanceof ReflectionSymbolId) {
125+
logger.warn(
126+
i18n.comment_for_0_links_to_1_not_included_in_docs_use_external_link_2(
127+
reflection.getFriendlyFullName(),
128+
linkText,
129+
`{ "${broken.target.packageName}": { "${broken.target.qualifiedName}": "#" }}`,
130+
),
131+
);
132+
} else if (linkText.startsWith("@") && !linkText.includes("!")) {
133+
logger.warn(
134+
i18n.failed_to_resolve_link_to_0_in_comment_for_1_may_have_meant_2(
135+
linkText,
136+
reflection.getFriendlyFullName(),
137+
linkText.replace(/[.#~]/, "!"),
138+
),
139+
);
140+
} else {
141+
logger.warn(
142+
i18n.failed_to_resolve_link_to_0_in_comment_for_1(
143+
linkText,
144+
reflection.getFriendlyFullName(),
145+
),
146+
);
147+
}
148+
}

src/test/behavior.c2.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,26 @@ describe("Behavior Tests", () => {
887887
]);
888888
});
889889

890+
it("Handles links which do not resolve correctly", () => {
891+
const project = convert("linkResolutionErrors");
892+
app.options.setValue("validation", {
893+
invalidLink: true,
894+
notDocumented: false,
895+
notExported: false,
896+
});
897+
898+
app.validate(project);
899+
logger.expectMessage(
900+
'warn: The comment for abc links to "Map.size" which was resolved but is not included in the documentation. To fix this warning export it or add { "typescript": { "Map.size": "#" }} to the externalSymbolLinkMappings option',
901+
);
902+
logger.expectMessage(
903+
'warn: Failed to resolve link to "DoesNotExist" in comment for abc',
904+
);
905+
logger.expectMessage(
906+
'warn: Failed to resolve link to "@typedoc/foo.DoesNotExist" in comment for abc. You may have wanted "@typedoc/foo!DoesNotExist"',
907+
);
908+
});
909+
890910
it("Handles merged declarations", () => {
891911
const project = convert("mergedDeclarations");
892912
const a = query(project, "SingleCommentMultiDeclaration");
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* {@link Map.size} TS resolves link, not included in docs #2700 #2967
3+
* {@link DoesNotExist} Symbol does not exist #2681
4+
* {@link @typedoc/foo.DoesNotExist} Symbol does not exist, looks like an attempt to link to a package directly #2360
5+
*/
6+
export const abc = new Map<string, number>();

src/test/converter2/issues/gh2681.ts

Lines changed: 0 additions & 4 deletions
This file was deleted.

src/test/issues.c2.test.ts

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import {
99
QueryType,
1010
ReferenceReflection,
1111
ReflectionKind,
12-
ReflectionSymbolId,
1312
ReflectionType,
1413
SignatureReflection,
1514
UnionType,
@@ -1671,16 +1670,6 @@ describe("Issue Tests", () => {
16711670
logger.expectNoOtherMessages();
16721671
});
16731672

1674-
it("#2681 reports warnings on @link tags which resolve to a type not included in the documentation", () => {
1675-
const project = convert();
1676-
app.options.setValue("validation", false);
1677-
app.options.setValue("validation", { invalidLink: true });
1678-
app.validate(project);
1679-
logger.expectMessage(
1680-
'warn: Failed to resolve link to "Generator" in comment for bug',
1681-
);
1682-
});
1683-
16841673
it("#2683 supports @param on parameters with functions", () => {
16851674
const project = convert();
16861675
const action = querySig(project, "action");
@@ -1733,31 +1722,7 @@ describe("Issue Tests", () => {
17331722
);
17341723
});
17351724

1736-
it("#2700a correctly parses links to global properties", () => {
1737-
const project = convert();
1738-
app.options.setValue("validation", {
1739-
invalidLink: true,
1740-
notDocumented: false,
1741-
notExported: false,
1742-
});
1743-
1744-
app.validate(project);
1745-
logger.expectMessage(
1746-
'warn: Failed to resolve link to "Map.size | size user specified" in comment for abc',
1747-
);
1748-
logger.expectMessage(
1749-
'warn: Failed to resolve link to "Map.size user specified" in comment for abc',
1750-
);
1751-
logger.expectMessage(
1752-
'warn: Failed to resolve link to "Map.size" in comment for abc',
1753-
);
1754-
1755-
const abc = query(project, "abc");
1756-
const link = abc.comment?.summary.find((c) => c.kind === "inline-tag");
1757-
ok(link?.target instanceof ReflectionSymbolId);
1758-
});
1759-
1760-
it("#2700b respects user specified link text when resolving external links", () => {
1725+
it("#2700 respects user specified link text when resolving external links", () => {
17611726
const project = convert();
17621727

17631728
const abc = query(project, "abc");

0 commit comments

Comments
 (0)