Skip to content

Commit

Permalink
Correctly handle scoped TS links
Browse files Browse the repository at this point in the history
Reported on Discord
  • Loading branch information
Gerrit0 committed Mar 26, 2023
1 parent f0d1e92 commit 1175431
Show file tree
Hide file tree
Showing 17 changed files with 207 additions and 190 deletions.
1 change: 1 addition & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"name": "Attach",
"port": 9229,
"request": "attach",
"internalConsoleOptions": "openOnSessionStart",
"skipFiles": ["<node_internals>/**"],
"type": "node",
"sourceMaps": true
Expand Down
3 changes: 2 additions & 1 deletion src/lib/converter/comments/blockLexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,9 +321,10 @@ function* lexBlockComment2(
if (link.name) {
const tsTarget = checker?.getSymbolAtLocation(link.name);
if (tsTarget) {
token.linkTarget = new ReflectionSymbolId(
token.tsLinkTarget = new ReflectionSymbolId(
resolveAliasedSymbol(tsTarget, checker!)
);
token.tsLinkText = link.text.replace(/^\s*\|\s*/, "");
}
}
}
Expand Down
21 changes: 14 additions & 7 deletions src/lib/converter/comments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,20 @@ const jsDocCommentKinds = [
ts.SyntaxKind.JSDocEnumTag,
];

const commentCache = new WeakMap<ts.SourceFile, Map<number, Comment>>();
let commentCache = new WeakMap<ts.SourceFile, Map<number, Comment>>();

// We need to do this for tests so that changing the tsLinkResolution option
// actually works. Without it, we'd get the old parsed comment which doesn't
// have the TS symbols attached.
export function clearCommentCache() {
commentCache = new WeakMap();
}

function getCommentWithCache(
discovered: DiscoveredComment | undefined,
config: CommentParserConfig,
logger: Logger,
checker: ts.TypeChecker
checker: ts.TypeChecker | undefined
) {
if (!discovered) return;

Expand Down Expand Up @@ -80,7 +87,7 @@ function getCommentImpl(
config: CommentParserConfig,
logger: Logger,
moduleComment: boolean,
checker: ts.TypeChecker
checker: ts.TypeChecker | undefined
) {
const comment = getCommentWithCache(commentSource, config, logger, checker);

Expand Down Expand Up @@ -114,7 +121,7 @@ export function getComment(
config: CommentParserConfig,
logger: Logger,
commentStyle: CommentStyle,
checker: ts.TypeChecker
checker: ts.TypeChecker | undefined
): Comment | undefined {
const declarations = symbol.declarations || [];

Expand Down Expand Up @@ -156,7 +163,7 @@ function getConstructorParamPropertyComment(
config: CommentParserConfig,
logger: Logger,
commentStyle: CommentStyle,
checker: ts.TypeChecker
checker: ts.TypeChecker | undefined
): Comment | undefined {
const decl = symbol.declarations?.find(ts.isParameter);
if (!decl) return;
Expand All @@ -181,7 +188,7 @@ export function getSignatureComment(
config: CommentParserConfig,
logger: Logger,
commentStyle: CommentStyle,
checker: ts.TypeChecker
checker: ts.TypeChecker | undefined
): Comment | undefined {
return getCommentImpl(
discoverSignatureComment(declaration, commentStyle),
Expand All @@ -201,7 +208,7 @@ export function getJsDocComment(
| ts.JSDocEnumTag,
config: CommentParserConfig,
logger: Logger,
checker: ts.TypeChecker
checker: ts.TypeChecker | undefined
): Comment | undefined {
const file = declaration.getSourceFile();

Expand Down
5 changes: 4 additions & 1 deletion src/lib/converter/comments/lexer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,8 @@ export interface Token {
text: string;

pos: number;
linkTarget?: ReflectionSymbolId;

// These come from the compiler for use if useTsLinkResolution is on
tsLinkTarget?: ReflectionSymbolId;
tsLinkText?: string;
}
96 changes: 63 additions & 33 deletions src/lib/converter/comments/linkResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,72 +16,71 @@ import { resolveDeclarationReference } from "./declarationReferenceResolver";
const urlPrefix = /^(http|ftp)s?:\/\//;

export type ExternalResolveResult = { target: string; caption?: string };

/**
* @param ref - Parsed declaration reference to resolve. This may be created automatically for some symbol, or
* parsed from user input.
* @param refl - Reflection that contains the resolved link
* @param part - If the declaration reference was created from a comment, the originating part.
* @param symbolId - If the declaration reference was created from a symbol, or `useTsLinkResolution` is turned
* on and TypeScript resolved the link to some symbol, the ID of that symbol.
*/
export type ExternalSymbolResolver = (
ref: DeclarationReference,
refl: Reflection,
part: Readonly<CommentDisplayPart> | undefined
part: Readonly<CommentDisplayPart> | undefined,
symbolId: ReflectionSymbolId | undefined
) => ExternalResolveResult | string | undefined;

export function resolveLinks(
comment: Comment,
reflection: Reflection,
externalResolver: ExternalSymbolResolver,
useTsResolution: boolean
externalResolver: ExternalSymbolResolver
) {
comment.summary = resolvePartLinks(
reflection,
comment.summary,
externalResolver,
useTsResolution
externalResolver
);
for (const tag of comment.blockTags) {
tag.content = resolvePartLinks(
reflection,
tag.content,
externalResolver,
useTsResolution
externalResolver
);
}

if (reflection instanceof DeclarationReflection && reflection.readme) {
reflection.readme = resolvePartLinks(
reflection,
reflection.readme,
externalResolver,
useTsResolution
externalResolver
);
}
}

export function resolvePartLinks(
reflection: Reflection,
parts: readonly CommentDisplayPart[],
externalResolver: ExternalSymbolResolver,
useTsResolution: boolean
externalResolver: ExternalSymbolResolver
): CommentDisplayPart[] {
return parts.flatMap((part) =>
processPart(reflection, part, externalResolver, useTsResolution)
processPart(reflection, part, externalResolver)
);
}

function processPart(
reflection: Reflection,
part: CommentDisplayPart,
externalResolver: ExternalSymbolResolver,
useTsResolution: boolean
externalResolver: ExternalSymbolResolver
): CommentDisplayPart | CommentDisplayPart[] {
if (part.kind === "inline-tag") {
if (
part.tag === "@link" ||
part.tag === "@linkcode" ||
part.tag === "@linkplain"
) {
return resolveLinkTag(
reflection,
part,
externalResolver,
useTsResolution
);
return resolveLinkTag(reflection, part, externalResolver);
}
}

Expand All @@ -91,9 +90,8 @@ function processPart(
function resolveLinkTag(
reflection: Reflection,
part: InlineTagDisplayPart,
externalResolver: ExternalSymbolResolver,
useTsResolution: boolean
) {
externalResolver: ExternalSymbolResolver
): InlineTagDisplayPart {
let defaultDisplayText = "";
let pos = 0;
const end = part.text.length;
Expand All @@ -102,19 +100,48 @@ function resolveLinkTag(
}

let target: Reflection | string | undefined;
if (useTsResolution && part.target instanceof ReflectionSymbolId) {
target = reflection.project.getReflectionFromSymbolId(part.target);
if (target) {
// Try to parse a declaration reference if we didn't use the TS symbol for resolution
const declRef = parseDeclarationReference(part.text, pos, end);

// Might already know where it should go if useTsLinkResolution is turned on
if (part.target instanceof ReflectionSymbolId) {
const tsTarget = reflection.project.getReflectionFromSymbolId(
part.target
);

if (tsTarget) {
target = tsTarget;
pos = end;
defaultDisplayText =
part.text.replace(/^\s*[A-Z_$][\w$]*[ |]*/i, "") || target.name;
defaultDisplayText = part.tsLinkText || target.name;
} else if (declRef) {
// If we didn't find a target, we might be pointing to a symbol in another project that will be merged in
// or some external symbol, so ask external resolvers to try resolution. Don't use regular declaration ref
// resolution in case it matches something that would have been merged in later.

const externalResolveResult = externalResolver(
declRef[0],
reflection,
part,
part.target instanceof ReflectionSymbolId
? part.target
: undefined
);

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

switch (typeof externalResolveResult) {
case "string":
target = externalResolveResult;
break;
case "object":
target = externalResolveResult.target;
defaultDisplayText =
externalResolveResult.caption || defaultDisplayText;
}
}
}

// Try to parse a declaration reference if we didn't use the TS symbol for resolution
const declRef = !target && parseDeclarationReference(part.text, pos, end);

if (declRef) {
if (!target && declRef) {
// Got one, great! Try to resolve the link
target = resolveDeclarationReference(reflection, declRef[0]);
pos = declRef[1];
Expand All @@ -126,7 +153,10 @@ function resolveLinkTag(
const externalResolveResult = externalResolver(
declRef[0],
reflection,
part
part,
part.target instanceof ReflectionSymbolId
? part.target
: undefined
);

defaultDisplayText = part.text.substring(0, pos);
Expand Down
5 changes: 3 additions & 2 deletions src/lib/converter/comments/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,8 +441,9 @@ function inlineTag(
tag: tagName.text as `@${string}`,
text: content.join(""),
};
if (tagName.linkTarget) {
inlineTag.target = tagName.linkTarget;
if (tagName.tsLinkTarget) {
inlineTag.target = tagName.tsLinkTarget;
inlineTag.tsLinkText = tagName.tsLinkText;
}
block.push(inlineTag);
}
45 changes: 42 additions & 3 deletions src/lib/converter/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { Converter } from "./converter";
import { isNamedNode } from "./utils/nodes";
import { ConverterEvents } from "./converter-events";
import { resolveAliasedSymbol } from "./utils/symbols";
import { getComment } from "./comments";
import { getComment, getJsDocComment, getSignatureComment } from "./comments";
import { getHumanName } from "../utils/tsutils";

/**
Expand Down Expand Up @@ -188,7 +188,7 @@ export class Context {
this.converter.config,
this.logger,
this.converter.commentStyle,
this.checker
this.converter.useTsLinkResolution ? this.checker : undefined
);
}
if (symbol && !reflection.comment) {
Expand All @@ -198,7 +198,7 @@ export class Context {
this.converter.config,
this.logger,
this.converter.commentStyle,
this.checker
this.converter.useTsLinkResolution ? this.checker : undefined
);
}

Expand Down Expand Up @@ -268,6 +268,45 @@ export class Context {
this._program = program;
}

getComment(symbol: ts.Symbol, kind: ReflectionKind) {
return getComment(
symbol,
kind,
this.converter.config,
this.logger,
this.converter.commentStyle,
this.converter.useTsLinkResolution ? this.checker : undefined
);
}

getJsDocComment(
declaration:
| ts.JSDocPropertyLikeTag
| ts.JSDocCallbackTag
| ts.JSDocTypedefTag
| ts.JSDocTemplateTag
| ts.JSDocEnumTag
) {
return getJsDocComment(
declaration,
this.converter.config,
this.logger,
this.converter.useTsLinkResolution ? this.checker : undefined
);
}

getSignatureComment(
declaration: ts.SignatureDeclaration | ts.JSDocSignature
) {
return getSignatureComment(
declaration,
this.converter.config,
this.logger,
this.converter.commentStyle,
this.converter.useTsLinkResolution ? this.checker : undefined
);
}

/**
* @param callback The callback function that should be executed with the changed context.
*/
Expand Down

0 comments on commit 1175431

Please sign in to comment.