Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 53 additions & 38 deletions src/lib/converter/comments/declarationReferenceResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
ReflectionKind,
} from "../../models/index.js";
import { assertNever, filterMap } from "#utils";
import type { ComponentPath, DeclarationReference, Meaning, MeaningKeyword } from "#utils";
import type { ComponentPath, DeclarationReference, Meaning } from "#utils";

function resolveReferenceReflection(ref: Reflection): Reflection {
if (ref instanceof ReferenceReflection) {
Expand All @@ -21,6 +21,7 @@ function resolveReferenceReflection(ref: Reflection): Reflection {
export function resolveDeclarationReference(
reflection: Reflection,
ref: DeclarationReference,
localRoots?: Reflection[],
): Reflection | undefined {
let high: Reflection[] = [];
let low: Reflection[] = [];
Expand All @@ -46,45 +47,47 @@ export function resolveDeclarationReference(
ref.resolutionStart.startsWith("local") &&
ref.resolutionStart.length === 5,
);
// TypeScript's behavior is to first try to resolve links via variable scope, and then
// if the link still hasn't been found, check either siblings (if comment belongs to a member)
// or otherwise children.
let refl: Reflection | undefined = reflection;
if (refl.kindOf(ReflectionKind.ExportContainer)) {
high.push(refl);
}
while (refl.parent) {
refl = refl.parent;
if (!localRoots) {
// TypeScript's behavior is to first try to resolve links via variable scope, and then
// if the link still hasn't been found, check either siblings (if comment belongs to a member)
// or otherwise children.
let refl: Reflection | undefined = reflection;
if (refl.kindOf(ReflectionKind.ExportContainer)) {
high.push(refl);
} else {
low.push(refl);
}
while (refl.parent) {
refl = refl.parent;
if (refl.kindOf(ReflectionKind.ExportContainer)) {
high.push(refl);
} else {
low.push(refl);
}

if (
refl.kindOf(ReflectionKind.Project) &&
(refl as ProjectReflection).children?.length === 1
) {
high.push((refl as ProjectReflection).children![0]);
if (
refl.kindOf(ReflectionKind.Project) &&
(refl as ProjectReflection).children?.length === 1
) {
high.push((refl as ProjectReflection).children![0]);
}
}
}

if (reflection.kindOf(ReflectionKind.SomeMember)) {
high.push(reflection.parent!);
} else if (
reflection.kindOf(ReflectionKind.SomeSignature) &&
reflection.parent!.kindOf(ReflectionKind.SomeMember)
) {
high.push(reflection.parent!.parent!);
} else if (high[0] !== reflection) {
if (reflection.parent instanceof ContainerReflection) {
high.push(
...(reflection.parent.childrenIncludingDocuments?.filter(
(c) => c.name === reflection.name,
) || []),
);
} else {
high.push(reflection);
if (reflection.kindOf(ReflectionKind.SomeMember)) {
high.push(reflection.parent!);
} else if (
reflection.kindOf(ReflectionKind.SomeSignature) &&
reflection.parent!.kindOf(ReflectionKind.SomeMember)
) {
high.push(reflection.parent!.parent!);
} else if (high[0] !== reflection) {
if (reflection.parent instanceof ContainerReflection) {
high.push(
...(reflection.parent.childrenIncludingDocuments?.filter(
(c) => c.name === reflection.name,
) || []),
);
} else {
high.push(reflection);
}
}
}
}
Expand All @@ -95,6 +98,11 @@ export function resolveDeclarationReference(
high = [];
const low2 = low;
low = [];
if (localRoots) {
high = localRoots;
localRoots = undefined;
continue;
}

for (const refl of high2) {
const resolved = resolveSymbolReferencePart(refl, part);
Expand Down Expand Up @@ -123,7 +131,7 @@ function filterMapByMeaning(
meaning: Meaning,
): Reflection[] {
return filterMap(reflections, (refl): Reflection | undefined => {
const kwResolved = resolveKeyword(refl, meaning.keyword) || [];
const kwResolved = resolveKeyword(refl, meaning) || [];
if (meaning.label) {
return kwResolved.find((r) => r.comment?.label === meaning.label);
}
Expand All @@ -133,9 +141,9 @@ function filterMapByMeaning(

function resolveKeyword(
refl: Reflection,
kw: MeaningKeyword | undefined,
meaning: Meaning,
): Reflection[] | undefined {
switch (kw) {
switch (meaning.keyword) {
case undefined:
return refl instanceof DeclarationReflection && refl.signatures
? refl.signatures
Expand All @@ -157,6 +165,13 @@ function resolveKeyword(
break;
case "function":
if (refl.kindOf(ReflectionKind.FunctionOrMethod)) {
// If the user uses :function, then we should target the function
// unless we have an index/label, in which case we should target
// the signatures, and the filterMapByMeaning function will further
// filter the signatures.
if (meaning.index == null && meaning.label == null) {
return [refl];
}
return (refl as DeclarationReflection).signatures;
}
break;
Expand Down Expand Up @@ -211,7 +226,7 @@ function resolveKeyword(
break;

default:
assertNever(kw);
assertNever(meaning.keyword);
}
}

Expand Down
46 changes: 43 additions & 3 deletions src/lib/converter/comments/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
import { lexLineComments } from "./lineLexer.js";
import { parseComment } from "./parser.js";
import type { FileRegistry } from "../../models/FileRegistry.js";
import { assertNever, i18n, Logger, setUnion } from "#utils";
import { assertNever, i18n, Logger, parseDeclarationReference, setUnion } from "#utils";
import { resolveAliasedSymbol } from "../utils/symbols.js";
import type { Context } from "../context.js";

export interface CommentParserConfig {
Expand All @@ -39,6 +40,7 @@ export interface CommentContextOptionalChecker {
config: CommentParserConfig;
logger: Logger;
checker?: ts.TypeChecker | undefined;
node?: ts.Node | undefined;
files: FileRegistry;
createSymbolId: Context["createSymbolId"];
}
Expand Down Expand Up @@ -85,13 +87,15 @@ function getCommentIgnoringCacheNoDiscoveryId(
file,
context,
);
resolveLocalLinks(comment, context);
break;
case ts.SyntaxKind.SingleLineCommentTrivia:
comment = parseComment(
lexLineComments(file.text, ranges),
file,
context,
);
resolveLocalLinks(comment, context);
break;
default:
assertNever(ranges[0].kind);
Expand All @@ -101,6 +105,37 @@ function getCommentIgnoringCacheNoDiscoveryId(
return comment;
}

function resolveLocalLinks(comment: Comment, context: CommentContextOptionalChecker) {
const { checker, node } = context;
if (!checker || !node) return;
for (const elt of comment.summary) {
if (elt.kind === "inline-tag" && elt.tag === "@link" && !elt.target) {
let pos = 0;
while (pos < elt.text.length && ts.isWhiteSpaceLike(elt.text.charCodeAt(pos))) {
pos++;
}
const parsed = parseDeclarationReference(elt.text, pos, elt.text.length);
if (parsed && parsed[0].resolutionStart === "local") {
const ref = parsed[0].symbolReference;
if (ref && ref.path) {
const symbol = checker.resolveName(
ref.path[0].path,
node,
ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace,
/* excludeGlobals */ false,
);
if (symbol) {
elt.baseSymbol = context.createSymbolId(resolveAliasedSymbol(symbol, checker));
if (ref.path.length === 1 && !ref.meaning) {
elt.target = elt.baseSymbol;
}
}
}
}
}
}
}

function getCommentWithCache(
discovered: DiscoveredComment | undefined,
context: CommentContextOptionalChecker,
Expand Down Expand Up @@ -128,13 +163,15 @@ function getCommentWithCache(
function getCommentImpl(
commentSource: DiscoveredComment | undefined,
moduleComment: boolean,
node: ts.Node | undefined,
context: CommentContext,
) {
const comment = getCommentWithCache(
commentSource,
{
...context,
checker: context.config.useTsLinkResolution ? context.checker : undefined,
node,
},
);

Expand Down Expand Up @@ -185,7 +222,7 @@ export function getComment(

const sf = declarations.find(ts.isSourceFile);
if (sf) {
return getFileComment(sf, context);
return getFileComment(sf, { ...context, node: sf });
}

const isModule = declarations.some((decl) => {
Expand All @@ -205,6 +242,7 @@ export function getComment(
!context.config.suppressCommentWarningsInDeclarationFiles,
),
isModule,
declarations.length ? declarations[0] : undefined,
context,
);

Expand All @@ -226,13 +264,14 @@ export function getNodeComment(
return getCommentImpl(
discoverNodeComment(node, context.config.commentStyle),
moduleComment,
node,
context,
);
}

export function getFileComment(
file: ts.SourceFile,
context: CommentContext,
context: CommentContextOptionalChecker,
): Comment | undefined {
const quietContext = {
...context,
Expand Down Expand Up @@ -295,6 +334,7 @@ export function getSignatureComment(
return getCommentImpl(
discoverSignatureComment(declaration, context.checker, context.config.commentStyle),
false,
declaration,
context,
);
}
Expand Down
6 changes: 4 additions & 2 deletions src/lib/converter/comments/linkResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,7 +222,7 @@ function resolveLinkTag(

return 1;
})!;
pos = end;
pos = declRef?.[1] ?? end;
defaultDisplayText = part.tsLinkText ||
(options.preserveLinkText ? part.text : target.name);
} else {
Expand Down Expand Up @@ -258,7 +258,9 @@ function resolveLinkTag(

if (!target && declRef) {
// Got one, great! Try to resolve the link
target = resolveDeclarationReference(reflection, declRef[0]);
let localRoots = part.baseSymbol && reflection.project.getReflectionsFromSymbolId(part.baseSymbol);
if (localRoots && localRoots.length === 0) localRoots = undefined;
target = resolveDeclarationReference(reflection, declRef[0], localRoots);
pos = declRef[1];

if (target) {
Expand Down
4 changes: 2 additions & 2 deletions src/lib/converter/comments/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,12 +792,12 @@ function inlineTag(
text: content.join(""),
};
if (tagName.tsLinkTarget) {
inlineTag.target = tagName.tsLinkTarget;
// inlineTag.target = tagName.tsLinkTarget;
}
// Separated from tsLinkTarget to avoid storing a useless empty string
// if TS doesn't have an opinion on what the link text should be.
if (tagName.tsLinkText) {
inlineTag.tsLinkText = tagName.tsLinkText;
// inlineTag.tsLinkText = tagName.tsLinkText;
}
block.push(inlineTag);
}
5 changes: 5 additions & 0 deletions src/lib/models/Comment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export interface InlineTagDisplayPart {
tag: TagString;
text: string;
target?: Reflection | string | ReflectionSymbolId;
baseSymbol?: ReflectionSymbolId;
tsLinkText?: string;
}

Expand Down Expand Up @@ -261,12 +262,14 @@ export class Comment {
case "code":
return { ...part };
case "inline-tag": {
const baseSymbol = part.baseSymbol && new ReflectionSymbolId(part.baseSymbol);
if (typeof part.target === "number") {
const part2 = {
kind: part.kind,
tag: part.tag,
text: part.text,
target: undefined,
baseSymbol,
tsLinkText: part.tsLinkText,
} satisfies InlineTagDisplayPart;
links.push([part.target, part2]);
Expand All @@ -280,6 +283,7 @@ export class Comment {
tag: part.tag,
text: part.text,
target: part.target,
baseSymbol,
tsLinkText: part.tsLinkText,
} satisfies InlineTagDisplayPart;
} else if (typeof part.target === "object") {
Expand All @@ -288,6 +292,7 @@ export class Comment {
tag: part.tag,
text: part.text,
target: new ReflectionSymbolId(part.target),
baseSymbol,
tsLinkText: part.tsLinkText,
} satisfies InlineTagDisplayPart;
} else {
Expand Down
27 changes: 27 additions & 0 deletions src/lib/models/kind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,33 @@ export enum ReflectionKind {
Document = 0x800000,
}

export const ReflectionKindNames = {
Project: Symbol.for(ReflectionKind[ReflectionKind.Project]),
Module: Symbol.for(ReflectionKind[ReflectionKind.Module]),
Namespace: Symbol.for(ReflectionKind[ReflectionKind.Namespace]),
Enum: Symbol.for(ReflectionKind[ReflectionKind.Enum]),
EnumMember: Symbol.for(ReflectionKind[ReflectionKind.EnumMember]),
Variable: Symbol.for(ReflectionKind[ReflectionKind.Variable]),
Function: Symbol.for(ReflectionKind[ReflectionKind.Function]),
Class: Symbol.for(ReflectionKind[ReflectionKind.Class]),
Interface: Symbol.for(ReflectionKind[ReflectionKind.Interface]),
Constructor: Symbol.for(ReflectionKind[ReflectionKind.Constructor]),
Property: Symbol.for(ReflectionKind[ReflectionKind.Property]),
Method: Symbol.for(ReflectionKind[ReflectionKind.Method]),
CallSignature: Symbol.for(ReflectionKind[ReflectionKind.CallSignature]),
IndexSignature: Symbol.for(ReflectionKind[ReflectionKind.IndexSignature]),
ConstructorSignature: Symbol.for(ReflectionKind[ReflectionKind.ConstructorSignature]),
Parameter: Symbol.for(ReflectionKind[ReflectionKind.Parameter]),
TypeLiteral: Symbol.for(ReflectionKind[ReflectionKind.TypeLiteral]),
TypeParameter: Symbol.for(ReflectionKind[ReflectionKind.TypeParameter]),
Accessor: Symbol.for(ReflectionKind[ReflectionKind.Accessor]),
GetSignature: Symbol.for(ReflectionKind[ReflectionKind.GetSignature]),
SetSignature: Symbol.for(ReflectionKind[ReflectionKind.SetSignature]),
TypeAlias: Symbol.for(ReflectionKind[ReflectionKind.TypeAlias]),
Reference: Symbol.for(ReflectionKind[ReflectionKind.Reference]),
Document: Symbol.for(ReflectionKind[ReflectionKind.Document]),
} as const;

/** @category Reflections */
export namespace ReflectionKind {
export type KindString = EnumKeys<typeof ReflectionKind>;
Expand Down
1 change: 1 addition & 0 deletions src/lib/serialization/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,7 @@ export interface InlineTagDisplayPart {
tag: TagString;
text: string;
target?: string | ReflectionId | ReflectionSymbolId;
baseSymbol?: ReflectionSymbolId;
tsLinkText?: string;
}

Expand Down
Loading
Loading