Skip to content

Commit 1175431

Browse files
committed
Correctly handle scoped TS links
Reported on Discord
1 parent f0d1e92 commit 1175431

File tree

17 files changed

+207
-190
lines changed

17 files changed

+207
-190
lines changed

.vscode/launch.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"name": "Attach",
2525
"port": 9229,
2626
"request": "attach",
27+
"internalConsoleOptions": "openOnSessionStart",
2728
"skipFiles": ["<node_internals>/**"],
2829
"type": "node",
2930
"sourceMaps": true

src/lib/converter/comments/blockLexer.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,10 @@ function* lexBlockComment2(
321321
if (link.name) {
322322
const tsTarget = checker?.getSymbolAtLocation(link.name);
323323
if (tsTarget) {
324-
token.linkTarget = new ReflectionSymbolId(
324+
token.tsLinkTarget = new ReflectionSymbolId(
325325
resolveAliasedSymbol(tsTarget, checker!)
326326
);
327+
token.tsLinkText = link.text.replace(/^\s*\|\s*/, "");
327328
}
328329
}
329330
}

src/lib/converter/comments/index.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,20 @@ const jsDocCommentKinds = [
2525
ts.SyntaxKind.JSDocEnumTag,
2626
];
2727

28-
const commentCache = new WeakMap<ts.SourceFile, Map<number, Comment>>();
28+
let commentCache = new WeakMap<ts.SourceFile, Map<number, Comment>>();
29+
30+
// We need to do this for tests so that changing the tsLinkResolution option
31+
// actually works. Without it, we'd get the old parsed comment which doesn't
32+
// have the TS symbols attached.
33+
export function clearCommentCache() {
34+
commentCache = new WeakMap();
35+
}
2936

3037
function getCommentWithCache(
3138
discovered: DiscoveredComment | undefined,
3239
config: CommentParserConfig,
3340
logger: Logger,
34-
checker: ts.TypeChecker
41+
checker: ts.TypeChecker | undefined
3542
) {
3643
if (!discovered) return;
3744

@@ -80,7 +87,7 @@ function getCommentImpl(
8087
config: CommentParserConfig,
8188
logger: Logger,
8289
moduleComment: boolean,
83-
checker: ts.TypeChecker
90+
checker: ts.TypeChecker | undefined
8491
) {
8592
const comment = getCommentWithCache(commentSource, config, logger, checker);
8693

@@ -114,7 +121,7 @@ export function getComment(
114121
config: CommentParserConfig,
115122
logger: Logger,
116123
commentStyle: CommentStyle,
117-
checker: ts.TypeChecker
124+
checker: ts.TypeChecker | undefined
118125
): Comment | undefined {
119126
const declarations = symbol.declarations || [];
120127

@@ -156,7 +163,7 @@ function getConstructorParamPropertyComment(
156163
config: CommentParserConfig,
157164
logger: Logger,
158165
commentStyle: CommentStyle,
159-
checker: ts.TypeChecker
166+
checker: ts.TypeChecker | undefined
160167
): Comment | undefined {
161168
const decl = symbol.declarations?.find(ts.isParameter);
162169
if (!decl) return;
@@ -181,7 +188,7 @@ export function getSignatureComment(
181188
config: CommentParserConfig,
182189
logger: Logger,
183190
commentStyle: CommentStyle,
184-
checker: ts.TypeChecker
191+
checker: ts.TypeChecker | undefined
185192
): Comment | undefined {
186193
return getCommentImpl(
187194
discoverSignatureComment(declaration, commentStyle),
@@ -201,7 +208,7 @@ export function getJsDocComment(
201208
| ts.JSDocEnumTag,
202209
config: CommentParserConfig,
203210
logger: Logger,
204-
checker: ts.TypeChecker
211+
checker: ts.TypeChecker | undefined
205212
): Comment | undefined {
206213
const file = declaration.getSourceFile();
207214

src/lib/converter/comments/lexer.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,8 @@ export interface Token {
1515
text: string;
1616

1717
pos: number;
18-
linkTarget?: ReflectionSymbolId;
18+
19+
// These come from the compiler for use if useTsLinkResolution is on
20+
tsLinkTarget?: ReflectionSymbolId;
21+
tsLinkText?: string;
1922
}

src/lib/converter/comments/linkResolver.ts

Lines changed: 63 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -16,72 +16,71 @@ import { resolveDeclarationReference } from "./declarationReferenceResolver";
1616
const urlPrefix = /^(http|ftp)s?:\/\//;
1717

1818
export type ExternalResolveResult = { target: string; caption?: string };
19+
20+
/**
21+
* @param ref - Parsed declaration reference to resolve. This may be created automatically for some symbol, or
22+
* parsed from user input.
23+
* @param refl - Reflection that contains the resolved link
24+
* @param part - If the declaration reference was created from a comment, the originating part.
25+
* @param symbolId - If the declaration reference was created from a symbol, or `useTsLinkResolution` is turned
26+
* on and TypeScript resolved the link to some symbol, the ID of that symbol.
27+
*/
1928
export type ExternalSymbolResolver = (
2029
ref: DeclarationReference,
2130
refl: Reflection,
22-
part: Readonly<CommentDisplayPart> | undefined
31+
part: Readonly<CommentDisplayPart> | undefined,
32+
symbolId: ReflectionSymbolId | undefined
2333
) => ExternalResolveResult | string | undefined;
2434

2535
export function resolveLinks(
2636
comment: Comment,
2737
reflection: Reflection,
28-
externalResolver: ExternalSymbolResolver,
29-
useTsResolution: boolean
38+
externalResolver: ExternalSymbolResolver
3039
) {
3140
comment.summary = resolvePartLinks(
3241
reflection,
3342
comment.summary,
34-
externalResolver,
35-
useTsResolution
43+
externalResolver
3644
);
3745
for (const tag of comment.blockTags) {
3846
tag.content = resolvePartLinks(
3947
reflection,
4048
tag.content,
41-
externalResolver,
42-
useTsResolution
49+
externalResolver
4350
);
4451
}
4552

4653
if (reflection instanceof DeclarationReflection && reflection.readme) {
4754
reflection.readme = resolvePartLinks(
4855
reflection,
4956
reflection.readme,
50-
externalResolver,
51-
useTsResolution
57+
externalResolver
5258
);
5359
}
5460
}
5561

5662
export function resolvePartLinks(
5763
reflection: Reflection,
5864
parts: readonly CommentDisplayPart[],
59-
externalResolver: ExternalSymbolResolver,
60-
useTsResolution: boolean
65+
externalResolver: ExternalSymbolResolver
6166
): CommentDisplayPart[] {
6267
return parts.flatMap((part) =>
63-
processPart(reflection, part, externalResolver, useTsResolution)
68+
processPart(reflection, part, externalResolver)
6469
);
6570
}
6671

6772
function processPart(
6873
reflection: Reflection,
6974
part: CommentDisplayPart,
70-
externalResolver: ExternalSymbolResolver,
71-
useTsResolution: boolean
75+
externalResolver: ExternalSymbolResolver
7276
): CommentDisplayPart | CommentDisplayPart[] {
7377
if (part.kind === "inline-tag") {
7478
if (
7579
part.tag === "@link" ||
7680
part.tag === "@linkcode" ||
7781
part.tag === "@linkplain"
7882
) {
79-
return resolveLinkTag(
80-
reflection,
81-
part,
82-
externalResolver,
83-
useTsResolution
84-
);
83+
return resolveLinkTag(reflection, part, externalResolver);
8584
}
8685
}
8786

@@ -91,9 +90,8 @@ function processPart(
9190
function resolveLinkTag(
9291
reflection: Reflection,
9392
part: InlineTagDisplayPart,
94-
externalResolver: ExternalSymbolResolver,
95-
useTsResolution: boolean
96-
) {
93+
externalResolver: ExternalSymbolResolver
94+
): InlineTagDisplayPart {
9795
let defaultDisplayText = "";
9896
let pos = 0;
9997
const end = part.text.length;
@@ -102,19 +100,48 @@ function resolveLinkTag(
102100
}
103101

104102
let target: Reflection | string | undefined;
105-
if (useTsResolution && part.target instanceof ReflectionSymbolId) {
106-
target = reflection.project.getReflectionFromSymbolId(part.target);
107-
if (target) {
103+
// Try to parse a declaration reference if we didn't use the TS symbol for resolution
104+
const declRef = parseDeclarationReference(part.text, pos, end);
105+
106+
// Might already know where it should go if useTsLinkResolution is turned on
107+
if (part.target instanceof ReflectionSymbolId) {
108+
const tsTarget = reflection.project.getReflectionFromSymbolId(
109+
part.target
110+
);
111+
112+
if (tsTarget) {
113+
target = tsTarget;
108114
pos = end;
109-
defaultDisplayText =
110-
part.text.replace(/^\s*[A-Z_$][\w$]*[ |]*/i, "") || target.name;
115+
defaultDisplayText = part.tsLinkText || target.name;
116+
} else if (declRef) {
117+
// If we didn't find a target, we might be pointing to a symbol in another project that will be merged in
118+
// or some external symbol, so ask external resolvers to try resolution. Don't use regular declaration ref
119+
// resolution in case it matches something that would have been merged in later.
120+
121+
const externalResolveResult = externalResolver(
122+
declRef[0],
123+
reflection,
124+
part,
125+
part.target instanceof ReflectionSymbolId
126+
? part.target
127+
: undefined
128+
);
129+
130+
defaultDisplayText = part.text.substring(0, pos);
131+
132+
switch (typeof externalResolveResult) {
133+
case "string":
134+
target = externalResolveResult;
135+
break;
136+
case "object":
137+
target = externalResolveResult.target;
138+
defaultDisplayText =
139+
externalResolveResult.caption || defaultDisplayText;
140+
}
111141
}
112142
}
113143

114-
// Try to parse a declaration reference if we didn't use the TS symbol for resolution
115-
const declRef = !target && parseDeclarationReference(part.text, pos, end);
116-
117-
if (declRef) {
144+
if (!target && declRef) {
118145
// Got one, great! Try to resolve the link
119146
target = resolveDeclarationReference(reflection, declRef[0]);
120147
pos = declRef[1];
@@ -126,7 +153,10 @@ function resolveLinkTag(
126153
const externalResolveResult = externalResolver(
127154
declRef[0],
128155
reflection,
129-
part
156+
part,
157+
part.target instanceof ReflectionSymbolId
158+
? part.target
159+
: undefined
130160
);
131161

132162
defaultDisplayText = part.text.substring(0, pos);

src/lib/converter/comments/parser.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -441,8 +441,9 @@ function inlineTag(
441441
tag: tagName.text as `@${string}`,
442442
text: content.join(""),
443443
};
444-
if (tagName.linkTarget) {
445-
inlineTag.target = tagName.linkTarget;
444+
if (tagName.tsLinkTarget) {
445+
inlineTag.target = tagName.tsLinkTarget;
446+
inlineTag.tsLinkText = tagName.tsLinkText;
446447
}
447448
block.push(inlineTag);
448449
}

src/lib/converter/context.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type { Converter } from "./converter";
1414
import { isNamedNode } from "./utils/nodes";
1515
import { ConverterEvents } from "./converter-events";
1616
import { resolveAliasedSymbol } from "./utils/symbols";
17-
import { getComment } from "./comments";
17+
import { getComment, getJsDocComment, getSignatureComment } from "./comments";
1818
import { getHumanName } from "../utils/tsutils";
1919

2020
/**
@@ -188,7 +188,7 @@ export class Context {
188188
this.converter.config,
189189
this.logger,
190190
this.converter.commentStyle,
191-
this.checker
191+
this.converter.useTsLinkResolution ? this.checker : undefined
192192
);
193193
}
194194
if (symbol && !reflection.comment) {
@@ -198,7 +198,7 @@ export class Context {
198198
this.converter.config,
199199
this.logger,
200200
this.converter.commentStyle,
201-
this.checker
201+
this.converter.useTsLinkResolution ? this.checker : undefined
202202
);
203203
}
204204

@@ -268,6 +268,45 @@ export class Context {
268268
this._program = program;
269269
}
270270

271+
getComment(symbol: ts.Symbol, kind: ReflectionKind) {
272+
return getComment(
273+
symbol,
274+
kind,
275+
this.converter.config,
276+
this.logger,
277+
this.converter.commentStyle,
278+
this.converter.useTsLinkResolution ? this.checker : undefined
279+
);
280+
}
281+
282+
getJsDocComment(
283+
declaration:
284+
| ts.JSDocPropertyLikeTag
285+
| ts.JSDocCallbackTag
286+
| ts.JSDocTypedefTag
287+
| ts.JSDocTemplateTag
288+
| ts.JSDocEnumTag
289+
) {
290+
return getJsDocComment(
291+
declaration,
292+
this.converter.config,
293+
this.logger,
294+
this.converter.useTsLinkResolution ? this.checker : undefined
295+
);
296+
}
297+
298+
getSignatureComment(
299+
declaration: ts.SignatureDeclaration | ts.JSDocSignature
300+
) {
301+
return getSignatureComment(
302+
declaration,
303+
this.converter.config,
304+
this.logger,
305+
this.converter.commentStyle,
306+
this.converter.useTsLinkResolution ? this.checker : undefined
307+
);
308+
}
309+
271310
/**
272311
* @param callback The callback function that should be executed with the changed context.
273312
*/

0 commit comments

Comments
 (0)