From f63875fc3d75db83468bf05bfb2a1dd11dda63ab Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Sat, 30 May 2026 15:44:41 +0200 Subject: [PATCH 1/5] WIP Resolve local links via TypeChecker Issue https://github.com/TypeStrong/typedoc/issues/3113 --- src/lib/converter/comments/index.ts | 84 ++++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/src/lib/converter/comments/index.ts b/src/lib/converter/comments/index.ts index 9470057a0..43a43289a 100644 --- a/src/lib/converter/comments/index.ts +++ b/src/lib/converter/comments/index.ts @@ -12,7 +12,16 @@ 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, + type MeaningKeyword, + parseDeclarationReference, + setUnion, + type SymbolReference, +} from "#utils"; +import { resolveAliasedSymbol } from "../utils/symbols.js"; import type { Context } from "../context.js"; export interface CommentParserConfig { @@ -39,6 +48,7 @@ export interface CommentContextOptionalChecker { config: CommentParserConfig; logger: Logger; checker?: ts.TypeChecker | undefined; + node?: ts.Node | undefined; files: FileRegistry; createSymbolId: Context["createSymbolId"]; } @@ -85,6 +95,7 @@ function getCommentIgnoringCacheNoDiscoveryId( file, context, ); + if (!jsDoc) resolveLocalLinks(comment, context); break; case ts.SyntaxKind.SingleLineCommentTrivia: comment = parseComment( @@ -92,6 +103,7 @@ function getCommentIgnoringCacheNoDiscoveryId( file, context, ); + resolveLocalLinks(comment, context); break; default: assertNever(ranges[0].kind); @@ -101,6 +113,71 @@ 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" && parsed[0].symbolReference) { + const resolved = resolveLocalLink(parsed[0].symbolReference, node, checker); + if (resolved) { + elt.target = context.createSymbolId(resolveAliasedSymbol(resolved, checker)); + } + } + } + } +} + +function resolveLocalLink(ref: SymbolReference, node: ts.Node, checker: ts.TypeChecker): ts.Symbol | undefined { + if (!ref.path || ref.meaning?.label) return undefined; + let symbols: ts.Symbol[] = []; + const add = (sym: ts.Symbol | undefined) => { + if (sym) { + sym = resolveAliasedSymbol(sym, checker); + if (!symbols.includes(sym)) symbols.push(sym); + } + }; + add(checker.resolveName(ref.path[0].path, node, ts.SymbolFlags.Value, true)); + add(checker.resolveName(ref.path[0].path, node, ts.SymbolFlags.Type, true)); + add(checker.resolveName(ref.path[0].path, node, ts.SymbolFlags.Namespace, true)); + for (let i = 1; i < ref.path.length && symbols.length; i++) { + const { path, navigation } = ref.path[i]; + const prev = symbols; + symbols = []; + for (const sym of prev) { + if (navigation !== "~") { + add(sym.members?.get(path as ts.__String)); + } + if (navigation !== "#") { + add(sym.exports?.get(path as ts.__String)); + } + } + } + const filter = ref.meaning?.keyword && meaningSymbolFlags[ref.meaning.keyword]; + if (filter != null) { + symbols = symbols.filter(sym => sym.flags & filter); + } + return symbols.length ? symbols[0] : undefined; +} + +const meaningSymbolFlags: { [meaning in MeaningKeyword]?: ts.SymbolFlags } = { + class: ts.SymbolFlags.Class, + interface: ts.SymbolFlags.Interface, + type: ts.SymbolFlags.Type, + enum: ts.SymbolFlags.Enum, + namespace: ts.SymbolFlags.Namespace, + function: ts.SymbolFlags.Function, + var: ts.SymbolFlags.Variable, + constructor: ts.SymbolFlags.Constructor, + member: ts.SymbolFlags.ClassMember | ts.SymbolFlags.EnumMember, + call: ts.SymbolFlags.Signature, +}; + function getCommentWithCache( discovered: DiscoveredComment | undefined, context: CommentContextOptionalChecker, @@ -128,6 +205,7 @@ function getCommentWithCache( function getCommentImpl( commentSource: DiscoveredComment | undefined, moduleComment: boolean, + node: ts.Node | undefined, context: CommentContext, ) { const comment = getCommentWithCache( @@ -135,6 +213,7 @@ function getCommentImpl( { ...context, checker: context.config.useTsLinkResolution ? context.checker : undefined, + node, }, ); @@ -205,6 +284,7 @@ export function getComment( !context.config.suppressCommentWarningsInDeclarationFiles, ), isModule, + declarations.length ? declarations[0] : undefined, context, ); @@ -226,6 +306,7 @@ export function getNodeComment( return getCommentImpl( discoverNodeComment(node, context.config.commentStyle), moduleComment, + node, context, ); } @@ -295,6 +376,7 @@ export function getSignatureComment( return getCommentImpl( discoverSignatureComment(declaration, context.checker, context.config.commentStyle), false, + declaration, context, ); } From b96db1e8635c6b40a4e7a772acde30110f6cd6f5 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Mon, 1 Jun 2026 09:50:42 +0200 Subject: [PATCH 2/5] Only call resolveName once --- src/lib/converter/comments/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib/converter/comments/index.ts b/src/lib/converter/comments/index.ts index 43a43289a..4db5b8392 100644 --- a/src/lib/converter/comments/index.ts +++ b/src/lib/converter/comments/index.ts @@ -142,9 +142,13 @@ function resolveLocalLink(ref: SymbolReference, node: ts.Node, checker: ts.TypeC if (!symbols.includes(sym)) symbols.push(sym); } }; - add(checker.resolveName(ref.path[0].path, node, ts.SymbolFlags.Value, true)); - add(checker.resolveName(ref.path[0].path, node, ts.SymbolFlags.Type, true)); - add(checker.resolveName(ref.path[0].path, node, ts.SymbolFlags.Namespace, true)); + // SymbolFlags.All does not work here, for some reason. + add(checker.resolveName( + ref.path[0].path, + node, + ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, + true, + )); for (let i = 1; i < ref.path.length && symbols.length; i++) { const { path, navigation } = ref.path[i]; const prev = symbols; From 5fa966cf5a2479d1d40bab07d836c4da8208fdf5 Mon Sep 17 00:00:00 2001 From: Marijn Haverbeke Date: Tue, 2 Jun 2026 09:52:27 +0200 Subject: [PATCH 3/5] Only look up the path's root via TypeScript, let regular resolver do the rest --- .../comments/declarationReferenceResolver.ts | 74 ++++++++++-------- src/lib/converter/comments/index.ts | 75 ++++--------------- src/lib/converter/comments/linkResolver.ts | 4 +- src/lib/models/Comment.ts | 5 ++ src/lib/serialization/schema.ts | 1 + 5 files changed, 63 insertions(+), 96 deletions(-) diff --git a/src/lib/converter/comments/declarationReferenceResolver.ts b/src/lib/converter/comments/declarationReferenceResolver.ts index 1cebd1c0e..f067ad1a5 100644 --- a/src/lib/converter/comments/declarationReferenceResolver.ts +++ b/src/lib/converter/comments/declarationReferenceResolver.ts @@ -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[] = []; @@ -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); + } } } } @@ -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); diff --git a/src/lib/converter/comments/index.ts b/src/lib/converter/comments/index.ts index 4db5b8392..cce8609c1 100644 --- a/src/lib/converter/comments/index.ts +++ b/src/lib/converter/comments/index.ts @@ -12,15 +12,7 @@ import { import { lexLineComments } from "./lineLexer.js"; import { parseComment } from "./parser.js"; import type { FileRegistry } from "../../models/FileRegistry.js"; -import { - assertNever, - i18n, - Logger, - type MeaningKeyword, - parseDeclarationReference, - setUnion, - type SymbolReference, -} from "#utils"; +import { assertNever, i18n, Logger, parseDeclarationReference, setUnion } from "#utils"; import { resolveAliasedSymbol } from "../utils/symbols.js"; import type { Context } from "../context.js"; @@ -123,65 +115,24 @@ function resolveLocalLinks(comment: Comment, context: CommentContextOptionalChec pos++; } const parsed = parseDeclarationReference(elt.text, pos, elt.text.length); - if (parsed && parsed[0].resolutionStart === "local" && parsed[0].symbolReference) { - const resolved = resolveLocalLink(parsed[0].symbolReference, node, checker); - if (resolved) { - elt.target = context.createSymbolId(resolveAliasedSymbol(resolved, checker)); + 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, + true, + ); + if (symbol) { + elt.localSymbol = context.createSymbolId(resolveAliasedSymbol(symbol, checker)); + } } } } } } -function resolveLocalLink(ref: SymbolReference, node: ts.Node, checker: ts.TypeChecker): ts.Symbol | undefined { - if (!ref.path || ref.meaning?.label) return undefined; - let symbols: ts.Symbol[] = []; - const add = (sym: ts.Symbol | undefined) => { - if (sym) { - sym = resolveAliasedSymbol(sym, checker); - if (!symbols.includes(sym)) symbols.push(sym); - } - }; - // SymbolFlags.All does not work here, for some reason. - add(checker.resolveName( - ref.path[0].path, - node, - ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, - true, - )); - for (let i = 1; i < ref.path.length && symbols.length; i++) { - const { path, navigation } = ref.path[i]; - const prev = symbols; - symbols = []; - for (const sym of prev) { - if (navigation !== "~") { - add(sym.members?.get(path as ts.__String)); - } - if (navigation !== "#") { - add(sym.exports?.get(path as ts.__String)); - } - } - } - const filter = ref.meaning?.keyword && meaningSymbolFlags[ref.meaning.keyword]; - if (filter != null) { - symbols = symbols.filter(sym => sym.flags & filter); - } - return symbols.length ? symbols[0] : undefined; -} - -const meaningSymbolFlags: { [meaning in MeaningKeyword]?: ts.SymbolFlags } = { - class: ts.SymbolFlags.Class, - interface: ts.SymbolFlags.Interface, - type: ts.SymbolFlags.Type, - enum: ts.SymbolFlags.Enum, - namespace: ts.SymbolFlags.Namespace, - function: ts.SymbolFlags.Function, - var: ts.SymbolFlags.Variable, - constructor: ts.SymbolFlags.Constructor, - member: ts.SymbolFlags.ClassMember | ts.SymbolFlags.EnumMember, - call: ts.SymbolFlags.Signature, -}; - function getCommentWithCache( discovered: DiscoveredComment | undefined, context: CommentContextOptionalChecker, diff --git a/src/lib/converter/comments/linkResolver.ts b/src/lib/converter/comments/linkResolver.ts index 45a0f9482..0267aa143 100644 --- a/src/lib/converter/comments/linkResolver.ts +++ b/src/lib/converter/comments/linkResolver.ts @@ -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.localSymbol && reflection.project.getReflectionsFromSymbolId(part.localSymbol); + if (localRoots && localRoots.length === 0) localRoots = undefined; + target = resolveDeclarationReference(reflection, declRef[0], localRoots); pos = declRef[1]; if (target) { diff --git a/src/lib/models/Comment.ts b/src/lib/models/Comment.ts index 613ab11f2..5dc255b72 100644 --- a/src/lib/models/Comment.ts +++ b/src/lib/models/Comment.ts @@ -38,6 +38,7 @@ export interface InlineTagDisplayPart { tag: TagString; text: string; target?: Reflection | string | ReflectionSymbolId; + localSymbol?: ReflectionSymbolId; tsLinkText?: string; } @@ -261,12 +262,14 @@ export class Comment { case "code": return { ...part }; case "inline-tag": { + const localSymbol = part.localSymbol && new ReflectionSymbolId(part.localSymbol); if (typeof part.target === "number") { const part2 = { kind: part.kind, tag: part.tag, text: part.text, target: undefined, + localSymbol, tsLinkText: part.tsLinkText, } satisfies InlineTagDisplayPart; links.push([part.target, part2]); @@ -280,6 +283,7 @@ export class Comment { tag: part.tag, text: part.text, target: part.target, + localSymbol, tsLinkText: part.tsLinkText, } satisfies InlineTagDisplayPart; } else if (typeof part.target === "object") { @@ -288,6 +292,7 @@ export class Comment { tag: part.tag, text: part.text, target: new ReflectionSymbolId(part.target), + localSymbol, tsLinkText: part.tsLinkText, } satisfies InlineTagDisplayPart; } else { diff --git a/src/lib/serialization/schema.ts b/src/lib/serialization/schema.ts index d5d03f41c..8ba36f78e 100644 --- a/src/lib/serialization/schema.ts +++ b/src/lib/serialization/schema.ts @@ -394,6 +394,7 @@ export interface InlineTagDisplayPart { tag: TagString; text: string; target?: string | ReflectionId | ReflectionSymbolId; + localSymbol?: ReflectionSymbolId; tsLinkText?: string; } From 2966cc901d534a98ee110ac0d78fd59ac6bf62c6 Mon Sep 17 00:00:00 2001 From: Gerrit Birkeland Date: Sun, 7 Jun 2026 18:44:17 -0600 Subject: [PATCH 4/5] WIP: Replace ts.JSDoc implementation with type checker resolution for root --- src/lib/converter/comments/index.ts | 22 +++- src/lib/converter/comments/parser.ts | 4 +- src/lib/models/kind.ts | 27 +++++ src/lib/utils-common/declarationReference.ts | 14 +++ src/lib/validation/links.ts | 14 ++- src/test/behavior.c2.test.ts | 110 +++++++++++-------- src/test/issues.c2.test.ts | 6 +- 7 files changed, 138 insertions(+), 59 deletions(-) diff --git a/src/lib/converter/comments/index.ts b/src/lib/converter/comments/index.ts index cce8609c1..49c85f4ce 100644 --- a/src/lib/converter/comments/index.ts +++ b/src/lib/converter/comments/index.ts @@ -1,5 +1,5 @@ import ts from "typescript"; -import { Comment, type CommentDisplayPart, ReflectionKind } from "../../models/index.js"; +import { Comment, type CommentDisplayPart, ReflectionKind, ReflectionSymbolId } from "../../models/index.js"; import type { CommentStyle, JsDocCompatibility, ValidationOptions } from "../../utils/options/declaration.js"; import { lexBlockComment } from "./blockLexer.js"; import { @@ -87,7 +87,7 @@ function getCommentIgnoringCacheNoDiscoveryId( file, context, ); - if (!jsDoc) resolveLocalLinks(comment, context); + resolveLocalLinks(comment, context); break; case ts.SyntaxKind.SingleLineCommentTrivia: comment = parseComment( @@ -121,11 +121,21 @@ function resolveLocalLinks(comment: Comment, context: CommentContextOptionalChec const symbol = checker.resolveName( ref.path[0].path, node, - ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, - true, + ts.SymbolFlags.All, + /* excludeGlobals */ false, ); if (symbol) { elt.localSymbol = context.createSymbolId(resolveAliasedSymbol(symbol, checker)); + if (ref.path.length > 1 && !ref.meaning) { + elt.target = new ReflectionSymbolId({ + packageName: elt.localSymbol.packageName, + packagePath: elt.localSymbol.packagePath, + qualifiedName: elt.localSymbol.qualifiedName + "." + + ref.path.slice(1).map(s => s.path).join("."), + }); + } else if (!ref.meaning) { + elt.target = elt.localSymbol; + } } } } @@ -219,7 +229,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) => { @@ -268,7 +278,7 @@ export function getNodeComment( export function getFileComment( file: ts.SourceFile, - context: CommentContext, + context: CommentContextOptionalChecker, ): Comment | undefined { const quietContext = { ...context, diff --git a/src/lib/converter/comments/parser.ts b/src/lib/converter/comments/parser.ts index b481118f3..020a5bbca 100644 --- a/src/lib/converter/comments/parser.ts +++ b/src/lib/converter/comments/parser.ts @@ -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); } diff --git a/src/lib/models/kind.ts b/src/lib/models/kind.ts index 534cdce8c..557291d7b 100644 --- a/src/lib/models/kind.ts +++ b/src/lib/models/kind.ts @@ -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; diff --git a/src/lib/utils-common/declarationReference.ts b/src/lib/utils-common/declarationReference.ts index c0c8bace9..bb9ff4778 100644 --- a/src/lib/utils-common/declarationReference.ts +++ b/src/lib/utils-common/declarationReference.ts @@ -55,6 +55,20 @@ export function meaningToString(meaning: Meaning): string { return result; } +export function symbolReferenceToString(ref: SymbolReference): string { + const parts: string[] = []; + for (const part of ref.path || []) { + if (parts.length) { + parts.push(part.navigation); + } + parts.push(part.path); // TODO escapes + } + if (ref.meaning) { + parts.push(meaningToString(ref.meaning)); + } + return parts.join(""); +} + export interface SymbolReference { path?: ComponentPath[]; meaning?: Meaning; diff --git a/src/lib/validation/links.ts b/src/lib/validation/links.ts index 64c5007d5..39e30a4c7 100644 --- a/src/lib/validation/links.ts +++ b/src/lib/validation/links.ts @@ -1,4 +1,4 @@ -import { i18n, type Logger } from "#utils"; +import { assert, i18n, type Logger, parseDeclarationReference, symbolReferenceToString } from "#utils"; import { type Comment, type CommentDisplayPart, @@ -125,6 +125,18 @@ function reportBrokenCommentLink(broken: InlineTagDisplayPart, reflection: Refle `{ "${broken.target.packageName}": { "${broken.target.qualifiedName}": "#" }}`, ), ); + } else if (broken.localSymbol) { + const [declRef] = parseDeclarationReference(linkText, 0, linkText.length) || []; + assert(declRef, "We had to have successfully parsed to set localSymbol to begin with"); + assert(declRef.resolutionStart === "local" && declRef.symbolReference); + const qualifiedName = symbolReferenceToString(declRef.symbolReference); + logger.validationWarning( + i18n.comment_for_0_links_to_1_not_included_in_docs_use_external_link_2( + reflection.getFriendlyFullName(), + linkText, + `{ "${broken.localSymbol.packageName}": { ${JSON.stringify(qualifiedName)}: "#" }}`, + ), + ); } else if (linkText.startsWith("@") && !linkText.includes("!")) { logger.validationWarning( i18n.failed_to_resolve_link_to_0_in_comment_for_1_may_have_meant_2( diff --git a/src/test/behavior.c2.test.ts b/src/test/behavior.c2.test.ts index af2e6cb02..04fe0fd85 100644 --- a/src/test/behavior.c2.test.ts +++ b/src/test/behavior.c2.test.ts @@ -6,6 +6,7 @@ import { LiteralType, Reflection, ReflectionKind, + ReflectionKindNames, SignatureReflection, } from "../lib/models/index.js"; import { filterMap } from "#utils"; @@ -28,6 +29,14 @@ function buildNameTree( return tree; } +/** + * Gets a shorthand description of links within a reflection's comment summary. + * Returns an array of links: + * 1. [string] for url targets + * 2. [fullName, number] for signature targets + * 3. [kindSymbol, fullName] for reflection targets + * 4. [qualifiedName] for unresolved symbol ids + */ function getLinks(refl: Reflection) { ok(refl.comment); return filterMap(refl.comment.summary, (p) => { @@ -42,9 +51,12 @@ function getLinks(refl: Reflection) { ]; } if (p.target instanceof Reflection) { - return [p.target.kind, p.target.getFullName()]; + return [Symbol.for(ReflectionKind[p.target.kind]), p.target.getFullName()]; } - return [p.target?.qualifiedName]; + if (p.target) { + return `RID{${p.target.qualifiedName}}`; + } + return [undefined]; } }); } @@ -73,9 +85,11 @@ describe("Behavior Tests", () => { optionsSnap = app.options.snapshot(); }); - afterEach(() => { + afterEach(function () { app.options.restore(optionsSnap); - logger.expectNoOtherMessages(); + if (!this.currentTest?.isFailed()) { + logger.expectNoOtherMessages(); + } }); it("Handles 'as const' style enums", () => { @@ -737,23 +751,23 @@ describe("Behavior Tests", () => { const links = getLinks(query(project, "Meanings")); equal(links, [ - [ReflectionKind.Enum, "Meanings.A"], - [ReflectionKind.Namespace, "Meanings.A"], - [ReflectionKind.Enum, "Meanings.A"], + [ReflectionKindNames.Enum, "Meanings.A"], + [ReflectionKindNames.Namespace, "Meanings.A"], + [ReflectionKindNames.Enum, "Meanings.A"], [undefined], - [ReflectionKind.Class, "Meanings.B"], + [ReflectionKindNames.Class, "Meanings.B"], - [ReflectionKind.Interface, "Meanings.C"], - [ReflectionKind.TypeAlias, "Meanings.D"], + [ReflectionKindNames.Interface, "Meanings.C"], + [ReflectionKindNames.TypeAlias, "Meanings.D"], ["Meanings.E.E", 0], - [ReflectionKind.Variable, "Meanings.F"], + [ReflectionKindNames.Variable, "Meanings.F"], ["Meanings.B.constructor.B", 0], ["Meanings.B.constructor.B", 0], ["Meanings.B.constructor.B", 1], - [ReflectionKind.EnumMember, "Meanings.A.A"], + [ReflectionKindNames.EnumMember, "Meanings.A.A"], [undefined], ["Meanings.E.E", 0], @@ -763,10 +777,10 @@ describe("Behavior Tests", () => { ["Meanings.B.constructor.B", 1], ["Meanings.B.__index", undefined], - [ReflectionKind.Interface, "Meanings.G"], + [ReflectionKindNames.Interface, "Meanings.G"], ["Meanings.E.E", 1], - [ReflectionKind.Class, "Meanings.B"], + [ReflectionKindNames.Class, "Meanings.B"], ]); equal(getLinks(query(project, "URLS")), [ @@ -780,13 +794,13 @@ describe("Behavior Tests", () => { ); equal(getLinks(query(project, "Navigation")), [ - [ReflectionKind.Method, "Navigation.Child.foo"], - [ReflectionKind.Property, "Navigation.Child.foo"], + [ReflectionKindNames.Method, "Navigation.Child.foo"], + [ReflectionKindNames.Property, "Navigation.Child.foo"], [undefined], ]); const foo = query(project, "Navigation.Child.foo").signatures![0]; - equal(getLinks(foo), [[ReflectionKind.Method, "Navigation.Child.foo"]]); + equal(getLinks(foo), [[ReflectionKindNames.Method, "Navigation.Child.foo"]]); }); it("Handles TypeScript based link resolution", () => { @@ -809,36 +823,36 @@ describe("Behavior Tests", () => { const links = getLinks(query(project, "Meanings")); equal(links, [ - [ReflectionKind.Namespace, "Meanings"], - [ReflectionKind.Namespace, "Meanings"], - [ReflectionKind.Namespace, "Meanings"], + [ReflectionKindNames.Enum, "Meanings.A"], + [ReflectionKindNames.Namespace, "Meanings.A"], + [ReflectionKindNames.Enum, "Meanings.A"], - [ReflectionKind.Enum, "Meanings.A"], - [ReflectionKind.Class, "Meanings.B"], + [undefined], + [ReflectionKindNames.Class, "Meanings.B"], - [ReflectionKind.Interface, "Meanings.C"], - [ReflectionKind.TypeAlias, "Meanings.D"], - [ReflectionKind.Function, "Meanings.E"], - [ReflectionKind.Variable, "Meanings.F"], + [ReflectionKindNames.Interface, "Meanings.C"], + [ReflectionKindNames.TypeAlias, "Meanings.D"], + ["Meanings.E.E", 0], + [ReflectionKindNames.Variable, "Meanings.F"], - [ReflectionKind.Class, "Meanings.B"], - [ReflectionKind.Class, "Meanings.B"], - [ReflectionKind.Class, "Meanings.B"], + ["Meanings.B.constructor.B", 0], + ["Meanings.B.constructor.B", 0], + ["Meanings.B.constructor.B", 1], - [ReflectionKind.EnumMember, "Meanings.A.A"], - [ReflectionKind.Property, "Meanings.B.prop"], + [ReflectionKindNames.EnumMember, "Meanings.A.A"], + [undefined], - [ReflectionKind.Function, "Meanings.E"], - [ReflectionKind.Function, "Meanings.E"], + ["Meanings.E.E", 0], + ["Meanings.E.E", 1], - [ReflectionKind.Class, "Meanings.B"], - [ReflectionKind.Class, "Meanings.B"], + ["Meanings.B.constructor.B", 0], + ["Meanings.B.constructor.B", 1], - [ReflectionKind.Class, "Meanings.B"], - [ReflectionKind.Interface, "Meanings.G"], + ["Meanings.B.__index", undefined], + [ReflectionKindNames.Interface, "Meanings.G"], - [ReflectionKind.Function, "Meanings.E"], - [ReflectionKind.Class, "Meanings.B"], + ["Meanings.E.E", 1], + [ReflectionKindNames.Class, "Meanings.B"], ]); equal(getLinks(query(project, "URLS")), [ @@ -852,25 +866,25 @@ describe("Behavior Tests", () => { ); equal(getLinks(query(project, "Navigation")), [ - [ReflectionKind.Namespace, "Navigation"], - [ReflectionKind.Property, "Navigation.Child.foo"], - [ReflectionKind.Class, "Navigation.Child"], + [ReflectionKindNames.Method, "Navigation.Child.foo"], + [ReflectionKindNames.Property, "Navigation.Child.foo"], + [ReflectionKindNames.Class, "Navigation.Child"], ]); const foo = query(project, "Navigation.Child.foo").signatures![0]; - equal(getLinks(foo), [[ReflectionKind.Method, "Navigation.Child.foo"]]); + equal(getLinks(foo), [[ReflectionKindNames.Method, "Navigation.Child.foo"]]); const localSymbolRef = query(project, "localSymbolRef"); equal(getLinks(localSymbolRef), [ - [ReflectionKind.Variable, "A"], - [ReflectionKind.Variable, "A"], - [ReflectionKind.Variable, "A"], + [ReflectionKindNames.Variable, "A"], + [ReflectionKindNames.Variable, "A"], + [ReflectionKindNames.Variable, "A"], ]); equal(getLinkTexts(localSymbolRef), ["A!", "A2!", "AnotherName"]); equal(getLinks(query(project, "scoped")), [ - [ReflectionKind.Property, "Meanings.B.prop"], + [ReflectionKindNames.Property, "Meanings.B.prop"], ]); equal(getLinkTexts(query(project, "scoped")), ["p"]); }); @@ -881,7 +895,7 @@ describe("Behavior Tests", () => { // to GH2808DeeplyNestedLink.prop when rendering. equal(getLinks(query(project, "GH2808DeeplyNestedLink")), [ [ - ReflectionKind.Property, + ReflectionKindNames.Property, "GH2808DeeplyNestedLink.prop.__type.nested.__type.here", ], ]); diff --git a/src/test/issues.c2.test.ts b/src/test/issues.c2.test.ts index e6399ae6c..bc67342cc 100644 --- a/src/test/issues.c2.test.ts +++ b/src/test/issues.c2.test.ts @@ -40,9 +40,11 @@ describe("Issue Tests", () => { ); }); - afterEach(() => { + afterEach(function () { app.options.restore(optionsSnap); - logger.expectNoOtherMessages(); + if (!this.currentTest?.isFailed()) { + logger.expectNoOtherMessages(); + } }); it("#567", () => { From f70a6081cc329410d7213934273689b741fb5805 Mon Sep 17 00:00:00 2001 From: Gerrit Birkeland Date: Tue, 16 Jun 2026 20:52:35 -0600 Subject: [PATCH 5/5] Adjudicate and accept some symbol differences --- .../comments/declarationReferenceResolver.ts | 17 +++++-- src/lib/converter/comments/index.ts | 17 ++----- src/lib/converter/comments/linkResolver.ts | 4 +- src/lib/models/Comment.ts | 10 ++-- src/lib/serialization/schema.ts | 2 +- src/lib/utils-common/declarationReference.ts | 46 +++++++++++-------- src/lib/validation/links.ts | 6 +-- src/test/behavior.c2.test.ts | 21 +++++---- .../converter2/behavior/linkResolution.ts | 1 + src/test/declarationReference.test.ts | 16 +++++++ 10 files changed, 85 insertions(+), 55 deletions(-) diff --git a/src/lib/converter/comments/declarationReferenceResolver.ts b/src/lib/converter/comments/declarationReferenceResolver.ts index f067ad1a5..76ec180aa 100644 --- a/src/lib/converter/comments/declarationReferenceResolver.ts +++ b/src/lib/converter/comments/declarationReferenceResolver.ts @@ -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) { @@ -131,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); } @@ -141,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 @@ -165,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; @@ -219,7 +226,7 @@ function resolveKeyword( break; default: - assertNever(kw); + assertNever(meaning.keyword); } } diff --git a/src/lib/converter/comments/index.ts b/src/lib/converter/comments/index.ts index 49c85f4ce..4d81612e6 100644 --- a/src/lib/converter/comments/index.ts +++ b/src/lib/converter/comments/index.ts @@ -1,5 +1,5 @@ import ts from "typescript"; -import { Comment, type CommentDisplayPart, ReflectionKind, ReflectionSymbolId } from "../../models/index.js"; +import { Comment, type CommentDisplayPart, ReflectionKind } from "../../models/index.js"; import type { CommentStyle, JsDocCompatibility, ValidationOptions } from "../../utils/options/declaration.js"; import { lexBlockComment } from "./blockLexer.js"; import { @@ -121,20 +121,13 @@ function resolveLocalLinks(comment: Comment, context: CommentContextOptionalChec const symbol = checker.resolveName( ref.path[0].path, node, - ts.SymbolFlags.All, + ts.SymbolFlags.Value | ts.SymbolFlags.Type | ts.SymbolFlags.Namespace, /* excludeGlobals */ false, ); if (symbol) { - elt.localSymbol = context.createSymbolId(resolveAliasedSymbol(symbol, checker)); - if (ref.path.length > 1 && !ref.meaning) { - elt.target = new ReflectionSymbolId({ - packageName: elt.localSymbol.packageName, - packagePath: elt.localSymbol.packagePath, - qualifiedName: elt.localSymbol.qualifiedName + "." + - ref.path.slice(1).map(s => s.path).join("."), - }); - } else if (!ref.meaning) { - elt.target = elt.localSymbol; + elt.baseSymbol = context.createSymbolId(resolveAliasedSymbol(symbol, checker)); + if (ref.path.length === 1 && !ref.meaning) { + elt.target = elt.baseSymbol; } } } diff --git a/src/lib/converter/comments/linkResolver.ts b/src/lib/converter/comments/linkResolver.ts index 0267aa143..9b9768200 100644 --- a/src/lib/converter/comments/linkResolver.ts +++ b/src/lib/converter/comments/linkResolver.ts @@ -222,7 +222,7 @@ function resolveLinkTag( return 1; })!; - pos = end; + pos = declRef?.[1] ?? end; defaultDisplayText = part.tsLinkText || (options.preserveLinkText ? part.text : target.name); } else { @@ -258,7 +258,7 @@ function resolveLinkTag( if (!target && declRef) { // Got one, great! Try to resolve the link - let localRoots = part.localSymbol && reflection.project.getReflectionsFromSymbolId(part.localSymbol); + 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]; diff --git a/src/lib/models/Comment.ts b/src/lib/models/Comment.ts index 5dc255b72..3a75e3caa 100644 --- a/src/lib/models/Comment.ts +++ b/src/lib/models/Comment.ts @@ -38,7 +38,7 @@ export interface InlineTagDisplayPart { tag: TagString; text: string; target?: Reflection | string | ReflectionSymbolId; - localSymbol?: ReflectionSymbolId; + baseSymbol?: ReflectionSymbolId; tsLinkText?: string; } @@ -262,14 +262,14 @@ export class Comment { case "code": return { ...part }; case "inline-tag": { - const localSymbol = part.localSymbol && new ReflectionSymbolId(part.localSymbol); + 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, - localSymbol, + baseSymbol, tsLinkText: part.tsLinkText, } satisfies InlineTagDisplayPart; links.push([part.target, part2]); @@ -283,7 +283,7 @@ export class Comment { tag: part.tag, text: part.text, target: part.target, - localSymbol, + baseSymbol, tsLinkText: part.tsLinkText, } satisfies InlineTagDisplayPart; } else if (typeof part.target === "object") { @@ -292,7 +292,7 @@ export class Comment { tag: part.tag, text: part.text, target: new ReflectionSymbolId(part.target), - localSymbol, + baseSymbol, tsLinkText: part.tsLinkText, } satisfies InlineTagDisplayPart; } else { diff --git a/src/lib/serialization/schema.ts b/src/lib/serialization/schema.ts index 8ba36f78e..a205a4296 100644 --- a/src/lib/serialization/schema.ts +++ b/src/lib/serialization/schema.ts @@ -394,7 +394,7 @@ export interface InlineTagDisplayPart { tag: TagString; text: string; target?: string | ReflectionId | ReflectionSymbolId; - localSymbol?: ReflectionSymbolId; + baseSymbol?: ReflectionSymbolId; tsLinkText?: string; } diff --git a/src/lib/utils-common/declarationReference.ts b/src/lib/utils-common/declarationReference.ts index bb9ff4778..ab7f74d06 100644 --- a/src/lib/utils-common/declarationReference.ts +++ b/src/lib/utils-common/declarationReference.ts @@ -449,29 +449,37 @@ export function parseDeclarationReference( let resolutionStart: "global" | "local" = "local"; let topLevelLocalReference = false; - const moduleSourceOrSymbolRef = parseModuleSource(source, pos, end); - if (moduleSourceOrSymbolRef) { - if ( - moduleSourceOrSymbolRef[1] < end && - source[moduleSourceOrSymbolRef[1]] === "!" - ) { - // We had a module source! - pos = moduleSourceOrSymbolRef[1] + 1; - resolutionStart = "global"; - moduleSource = moduleSourceOrSymbolRef[0]; - - // We might be referencing a local of a module - if (source[pos] === "~") { - topLevelLocalReference = true; - pos++; + // First try to parse a symbol reference + const startingPos = pos; + const startingRef = parseSymbolReference(source, pos, end); + + // If the symbol reference ends at a "!" or we failed to parse anything and the start of the + // link is "!" then the first component is actually a module source, so we should parse that first. + if ((startingRef && source[startingRef[1]] === "!") || (!startingRef && source[pos] === "!")) { + const moduleSourceOrSymbolRef = parseModuleSource(source, pos, end); + if (moduleSourceOrSymbolRef) { + if ( + moduleSourceOrSymbolRef[1] < end && + source[moduleSourceOrSymbolRef[1]] === "!" + ) { + // We had a module source! + pos = moduleSourceOrSymbolRef[1] + 1; + resolutionStart = "global"; + moduleSource = moduleSourceOrSymbolRef[0]; + + // We might be referencing a local of a module + if (source[pos] === "~") { + topLevelLocalReference = true; + pos++; + } } + } else if (source[pos] === "!") { + pos++; + resolutionStart = "global"; } - } else if (source[pos] === "!") { - pos++; - resolutionStart = "global"; } - const ref = parseSymbolReference(source, pos, end); + const ref = pos === startingPos ? startingRef : parseSymbolReference(source, pos, end); if (ref) { symbolReference = ref[0]; if (topLevelLocalReference && symbolReference.path?.length) { diff --git a/src/lib/validation/links.ts b/src/lib/validation/links.ts index 39e30a4c7..f50c6a97a 100644 --- a/src/lib/validation/links.ts +++ b/src/lib/validation/links.ts @@ -125,16 +125,16 @@ function reportBrokenCommentLink(broken: InlineTagDisplayPart, reflection: Refle `{ "${broken.target.packageName}": { "${broken.target.qualifiedName}": "#" }}`, ), ); - } else if (broken.localSymbol) { + } else if (broken.baseSymbol) { const [declRef] = parseDeclarationReference(linkText, 0, linkText.length) || []; - assert(declRef, "We had to have successfully parsed to set localSymbol to begin with"); + assert(declRef, "We had to have successfully parsed to set baseSymbol to begin with"); assert(declRef.resolutionStart === "local" && declRef.symbolReference); const qualifiedName = symbolReferenceToString(declRef.symbolReference); logger.validationWarning( i18n.comment_for_0_links_to_1_not_included_in_docs_use_external_link_2( reflection.getFriendlyFullName(), linkText, - `{ "${broken.localSymbol.packageName}": { ${JSON.stringify(qualifiedName)}: "#" }}`, + `{ "${broken.baseSymbol.packageName}": { ${JSON.stringify(qualifiedName)}: "#" }}`, ), ); } else if (linkText.startsWith("@") && !linkText.includes("!")) { diff --git a/src/test/behavior.c2.test.ts b/src/test/behavior.c2.test.ts index 04fe0fd85..bf3d5d69b 100644 --- a/src/test/behavior.c2.test.ts +++ b/src/test/behavior.c2.test.ts @@ -32,7 +32,7 @@ function buildNameTree( /** * Gets a shorthand description of links within a reflection's comment summary. * Returns an array of links: - * 1. [string] for url targets + * 1. string for url targets * 2. [fullName, number] for signature targets * 3. [kindSymbol, fullName] for reflection targets * 4. [qualifiedName] for unresolved symbol ids @@ -760,7 +760,7 @@ describe("Behavior Tests", () => { [ReflectionKindNames.Interface, "Meanings.C"], [ReflectionKindNames.TypeAlias, "Meanings.D"], - ["Meanings.E.E", 0], + [ReflectionKindNames.Function, "Meanings.E"], [ReflectionKindNames.Variable, "Meanings.F"], ["Meanings.B.constructor.B", 0], @@ -772,6 +772,7 @@ describe("Behavior Tests", () => { ["Meanings.E.E", 0], ["Meanings.E.E", 1], + ["Meanings.E.E", 1], ["Meanings.B.constructor.B", 0], ["Meanings.B.constructor.B", 1], @@ -805,6 +806,7 @@ describe("Behavior Tests", () => { it("Handles TypeScript based link resolution", () => { app.options.setValue("sort", ["source-order"]); + const project = convert("linkResolutionTs"); for ( const [refl, target] of [ @@ -827,12 +829,12 @@ describe("Behavior Tests", () => { [ReflectionKindNames.Namespace, "Meanings.A"], [ReflectionKindNames.Enum, "Meanings.A"], - [undefined], + [undefined], // A:class doesn't exist, should fail [ReflectionKindNames.Class, "Meanings.B"], [ReflectionKindNames.Interface, "Meanings.C"], [ReflectionKindNames.TypeAlias, "Meanings.D"], - ["Meanings.E.E", 0], + [ReflectionKindNames.Function, "Meanings.E"], [ReflectionKindNames.Variable, "Meanings.F"], ["Meanings.B.constructor.B", 0], @@ -840,10 +842,11 @@ describe("Behavior Tests", () => { ["Meanings.B.constructor.B", 1], [ReflectionKindNames.EnumMember, "Meanings.A.A"], - [undefined], + [undefined], // B.prop:event is intentionally not permitted by TypeDoc ["Meanings.E.E", 0], ["Meanings.E.E", 1], + ["Meanings.E.E", 1], ["Meanings.B.constructor.B", 0], ["Meanings.B.constructor.B", 1], @@ -866,13 +869,15 @@ describe("Behavior Tests", () => { ); equal(getLinks(query(project, "Navigation")), [ - [ReflectionKindNames.Method, "Navigation.Child.foo"], + [ReflectionKindNames.Method, "Navigation.Child.foo"], // [ReflectionKind.Namespace, "Navigation"], [ReflectionKindNames.Property, "Navigation.Child.foo"], - [ReflectionKindNames.Class, "Navigation.Child"], + [undefined], ]); const foo = query(project, "Navigation.Child.foo").signatures![0]; - equal(getLinks(foo), [[ReflectionKindNames.Method, "Navigation.Child.foo"]]); + equal(getLinks(foo), [ + [ReflectionKindNames.Method, "Navigation.Child.foo"], + ]); const localSymbolRef = query(project, "localSymbolRef"); diff --git a/src/test/converter2/behavior/linkResolution.ts b/src/test/converter2/behavior/linkResolution.ts index 731223a02..7d1a630b0 100644 --- a/src/test/converter2/behavior/linkResolution.ts +++ b/src/test/converter2/behavior/linkResolution.ts @@ -45,6 +45,7 @@ export namespace Scoping { * * {@link E:call} * {@link E:call(1)} + * {@link E:function(1)} * * {@link B:new} * {@link B:new(1)} diff --git a/src/test/declarationReference.test.ts b/src/test/declarationReference.test.ts index a01e8890e..4986168a0 100644 --- a/src/test/declarationReference.test.ts +++ b/src/test/declarationReference.test.ts @@ -226,6 +226,22 @@ describe("Declaration References", () => { }, }); }); + + it("Correctly handles an ambiguous grammar", () => { + const src = "AnotherName | A!"; + const result = parseDeclarationReference(src, 0, src.length); + equal(result?.[0], { + moduleSource: undefined, + resolutionStart: "local", + symbolReference: { + path: [ + { navigation: ".", path: "AnotherName" }, + ], + meaning: undefined, + }, + }); + equal(result[1], src.indexOf(" ")); + }); }); describe("meaningToString", () => {