Skip to content

Commit 9189a4c

Browse files
committed
Improve handling of external link resolvers
Noticed when investing adding symbol ID handling in typedoc-plugin-mdn-links as originally noted was missing in #2700.
1 parent e0e5923 commit 9189a4c

File tree

9 files changed

+120
-24
lines changed

9 files changed

+120
-24
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Unreleased
22

3+
### Bug Fixes
4+
5+
- Correctly handle external link resolver link text when referencing an external symbol, #2700.
6+
- Corrected handling of `@link` tags present in comments at the start of source files.
7+
38
## v0.26.7 (2024-09-09)
49

510
### Features

src/lib/converter/comments/discovery.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,18 @@ function findJsDocForComment(
362362
.getJSDocCommentsAndTags(node)
363363
.map((doc) => ts.findAncestor(doc, ts.isJSDoc)) as ts.JSDoc[];
364364

365+
if (ts.isSourceFile(node)) {
366+
if (node.statements.length) {
367+
jsDocs.push(
368+
...(ts
369+
.getJSDocCommentsAndTags(node.statements[0])
370+
.map((doc) =>
371+
ts.findAncestor(doc, ts.isJSDoc),
372+
) as ts.JSDoc[]),
373+
);
374+
}
375+
}
376+
365377
return jsDocs.find((doc) => doc.pos === ranges[0].pos);
366378
}
367379
}

src/lib/converter/comments/linkResolver.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -141,23 +141,26 @@ function resolveLinkTag(
141141
defaultDisplayText =
142142
part.tsLinkText ||
143143
(options.preserveLinkText ? part.text : target.name);
144-
} else if (declRef) {
144+
} else {
145145
// If we didn't find a target, we might be pointing to a symbol in another project that will be merged in
146146
// or some external symbol, so ask external resolvers to try resolution. Don't use regular declaration ref
147147
// resolution in case it matches something that would have been merged in later.
148+
if (declRef) {
149+
pos = declRef[1];
150+
}
148151

149152
const externalResolveResult = externalResolver(
150-
declRef[0],
153+
declRef?.[0] ?? part.target.toDeclarationReference(),
151154
reflection,
152155
part,
153-
part.target instanceof ReflectionSymbolId
154-
? part.target
155-
: undefined,
156+
part.target,
156157
);
157158

158-
defaultDisplayText = options.preserveLinkText
159-
? part.text
160-
: part.text.substring(0, pos);
159+
defaultDisplayText =
160+
part.tsLinkText ||
161+
(options.preserveLinkText
162+
? part.text
163+
: part.text.substring(0, pos));
161164

162165
switch (typeof externalResolveResult) {
163166
case "string":

src/lib/converter/comments/parser.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -770,6 +770,10 @@ function inlineTag(
770770
};
771771
if (tagName.tsLinkTarget) {
772772
inlineTag.target = tagName.tsLinkTarget;
773+
}
774+
// Separated from tsLinkTarget to avoid storing a useless empty string
775+
// if TS doesn't have an opinion on what the link text should be.
776+
if (tagName.tsLinkText) {
773777
inlineTag.tsLinkText = tagName.tsLinkText;
774778
}
775779
block.push(inlineTag);

src/lib/models/reflections/ReflectionSymbolId.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,16 @@ import { existsSync } from "fs";
22
import { isAbsolute, join, relative, resolve } from "path";
33
import ts from "typescript";
44
import type { JSONOutput, Serializer } from "../../serialization/index";
5-
import { getCommonDirectory, readFile } from "../../utils/fs";
5+
import {
6+
findPackageForPath,
7+
getCommonDirectory,
8+
readFile,
9+
} from "../../utils/fs";
610
import { normalizePath } from "../../utils/paths";
711
import { getQualifiedName } from "../../utils/tsutils";
812
import { optional, validate } from "../../utils/validation";
13+
import type { DeclarationReference } from "../../converter/comments/declarationReference";
14+
import { splitUnquotedString } from "./utils";
915

1016
/**
1117
* See {@link ReflectionSymbolId}
@@ -78,6 +84,21 @@ export class ReflectionSymbolId {
7884
}
7985
}
8086

87+
toDeclarationReference(): DeclarationReference {
88+
return {
89+
resolutionStart: "global",
90+
moduleSource: findPackageForPath(this.fileName),
91+
symbolReference: {
92+
path: splitUnquotedString(this.qualifiedName, ".").map(
93+
(path) => ({
94+
navigation: ".",
95+
path,
96+
}),
97+
),
98+
},
99+
};
100+
}
101+
81102
toObject(serializer: Serializer) {
82103
const sourceFileName = isAbsolute(this.fileName)
83104
? normalizePath(

src/lib/models/types.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -928,21 +928,6 @@ export class ReferenceType extends Type {
928928
.fileName.replace(/\\/g, "/");
929929
if (!symbolPath) return ref;
930930

931-
// Attempt to decide package name from path if it contains "node_modules"
932-
let startIndex = symbolPath.lastIndexOf("node_modules/");
933-
if (startIndex !== -1) {
934-
startIndex += "node_modules/".length;
935-
let stopIndex = symbolPath.indexOf("/", startIndex);
936-
// Scoped package, e.g. `@types/node`
937-
if (symbolPath[startIndex] === "@") {
938-
stopIndex = symbolPath.indexOf("/", stopIndex + 1);
939-
}
940-
const packageName = symbolPath.substring(startIndex, stopIndex);
941-
ref.package = packageName;
942-
return ref;
943-
}
944-
945-
// Otherwise, look for a "package.json" file in a parent path
946931
ref.package = findPackageForPath(symbolPath);
947932
return ref;
948933
}

src/lib/utils/fs.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,18 @@ export function discoverPackageJson(dir: string) {
338338
const packageCache = new Map<string, string>();
339339

340340
export function findPackageForPath(sourcePath: string): string | undefined {
341+
// Attempt to decide package name from path if it contains "node_modules"
342+
let startIndex = sourcePath.lastIndexOf("node_modules/");
343+
if (startIndex !== -1) {
344+
startIndex += "node_modules/".length;
345+
let stopIndex = sourcePath.indexOf("/", startIndex);
346+
// Scoped package, e.g. `@types/node`
347+
if (sourcePath[startIndex] === "@") {
348+
stopIndex = sourcePath.indexOf("/", stopIndex + 1);
349+
}
350+
return sourcePath.substring(startIndex, stopIndex);
351+
}
352+
341353
const dir = dirname(sourcePath);
342354
const cache = packageCache.get(dir);
343355
if (cache) {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* {@link Map.size | size user specified}
3+
* {@link Map.size user specified}
4+
* {@link Map.size}
5+
*/
6+
export const abc = new Map<string, number>();

src/test/issues.c2.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
QueryType,
1717
ReferenceReflection,
1818
ReflectionKind,
19+
ReflectionSymbolId,
1920
ReflectionType,
2021
SignatureReflection,
2122
UnionType,
@@ -1732,4 +1733,51 @@ describe("Issue Tests", () => {
17321733
[undefined, "2", '"counterclockwise"'],
17331734
);
17341735
});
1736+
1737+
it("#2700a correctly parses links to global properties", () => {
1738+
const project = convert();
1739+
app.options.setValue("validation", {
1740+
invalidLink: true,
1741+
notDocumented: false,
1742+
notExported: false,
1743+
});
1744+
1745+
app.validate(project);
1746+
logger.expectMessage(
1747+
'warn: Failed to resolve link to "Map.size | size user specified" in comment for abc',
1748+
);
1749+
logger.expectMessage(
1750+
'warn: Failed to resolve link to "Map.size user specified" in comment for abc',
1751+
);
1752+
logger.expectMessage(
1753+
'warn: Failed to resolve link to "Map.size" in comment for abc',
1754+
);
1755+
1756+
const abc = query(project, "abc");
1757+
const link = abc.comment?.summary.find((c) => c.kind === "inline-tag");
1758+
ok(link?.target instanceof ReflectionSymbolId);
1759+
});
1760+
1761+
it("#2700b respects user specified link text when resolving external links", () => {
1762+
const project = convert();
1763+
1764+
const abc = query(project, "abc");
1765+
ok(abc.comment);
1766+
1767+
const resolvers = app.converter["_externalSymbolResolvers"].slice();
1768+
app.converter.addUnknownSymbolResolver(() => {
1769+
return {
1770+
target: "https://typedoc.org",
1771+
caption: "resolver caption",
1772+
};
1773+
});
1774+
app.converter.resolveLinks(abc.comment, abc);
1775+
app.converter["_externalSymbolResolvers"] = resolvers;
1776+
1777+
equal(getLinks(abc), [
1778+
{ display: "size user specified", target: "https://typedoc.org" },
1779+
{ display: "user specified", target: "https://typedoc.org" },
1780+
{ display: "resolver caption", target: "https://typedoc.org" },
1781+
]);
1782+
});
17351783
});

0 commit comments

Comments
 (0)