diff --git a/.config/typedoc.json b/.config/typedoc.json index 82510943b..00b57a15c 100644 --- a/.config/typedoc.json +++ b/.config/typedoc.json @@ -15,6 +15,8 @@ ], "name": "TypeDoc API", + // Don't document the debug entry point + "entryPoints": ["../src/index.ts"], "outputs": [ { "name": "html", diff --git a/CHANGELOG.md b/CHANGELOG.md index 4592091ef..fe510ef9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ title: Changelog ### Bug Fixes +- Possibly Breaking: TypeDoc will no longer render anchors within the page for + deeply nested properties. This only affects links to properties of + properties of types, which did not have a clickable link exposed so are + unlikely to have been linked to. Furthermore, these links were not always + created by TypeDoc, only being created if all parent properties contained + comments, #2808. +- TypeDoc will now warn if a property which does not have a URL within the + rendered document and the parent property/page will be linked to instead, + #2808. These warnings can be disabled with the `validation.rewrittenLink` + option. - Fix restoration of groups/categories including documents, #2801. - Fixed missed relative paths within markdown link references in documents. - Improved handling of incomplete inline code blocks within markdown. diff --git a/package.json b/package.json index 8f25a6d2d..07ce012fb 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "exports": { ".": "./dist/index.js", "./tsdoc.json": "./tsdoc.json", - "./package.json": "./package.json" + "./package.json": "./package.json", + "./debug": "./dist/lib/debug/index.js" }, "types": "./dist/index.d.ts", "bin": { diff --git a/scripts/capture_screenshots.mjs b/scripts/capture_screenshots.mjs index 3e01a5da7..07f213368 100644 --- a/scripts/capture_screenshots.mjs +++ b/scripts/capture_screenshots.mjs @@ -93,6 +93,9 @@ export async function captureScreenshots( headless, theme, ) { + await fs.promises.rm(outputDirectory, { force: true, recursive: true }); + await fs.promises.mkdir(outputDirectory, { recursive: true }); + const browser = await puppeteer.launch({ args: platform() === "win32" diff --git a/site/development/plugins.md b/site/development/plugins.md index 093872137..02a14d5b5 100644 --- a/site/development/plugins.md +++ b/site/development/plugins.md @@ -13,7 +13,8 @@ to. Plugins should assume that they may be loaded multiple times for different applications, and that a single load of an application class may be used to convert multiple projects. -Plugins may be either ESM or CommonJS. +Plugins may be either ESM or CommonJS, but TypeDoc ships with ESM, so they should +generally published as ESM to avoid `require(esm)` experimental warnings. ```js // @ts-check diff --git a/site/options/validation.md b/site/options/validation.md index c2cfd2731..00fc6810f 100644 --- a/site/options/validation.md +++ b/site/options/validation.md @@ -20,13 +20,32 @@ typedoc.json (defaults): "validation": { "notExported": true, "invalidLink": true, + "rewrittenLink": true, "notDocumented": false, "unusedMergeModuleWith": true } } ``` -Specifies validation steps TypeDoc should perform on your generated documentation. +Specifies validation steps TypeDoc should perform on your generated +documentation. Most validation occurs before rendering, but `rewrittenLink` is +done during HTML rendering as links have not been generated before rendering +begins. + +- **notExported** - Produce warnings if a type is referenced by the + documentation but the type isn't exported and therefore included in the + documentation. +- **invalidLink** - Produce warnings for `@link` tags which cannot be resolved. +- **rewrittenLink** - Produce warnings for `@link` tags which are resolved, + but whose target does not have a unique URL in the documentation. TypeDoc + will rewrite these links to point to the first parent with a URL. +- **notDocumented** - Produce warnings for reflections which do not have a + documentation comment. This is also controlled by the + [requiredToBeDocumented](#requiredtobedocumented) option. +- **unusedMergeModuleWith** - Produce warnings for + [`@mergeModuleWith`](../tags/mergeModuleWith.md) tags which are not + resolved. This option should generally be disabled if generating JSON which + will be combined with another document later. ## treatWarningsAsErrors diff --git a/src/index.ts b/src/index.ts index 4b4c7fd43..cf2415d5e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,10 @@ /** * @module TypeDoc API + * + * In addition to the members documented here, TypeDoc exports a `typedoc/debug` + * entry point which exports some functions which may be useful during plugin + * development or debugging. Exports from that entry point are **not stable** + * and may change or be removed at any time. */ export { Application, type ApplicationEvents } from "./lib/application.js"; diff --git a/src/lib/converter/comments/declarationReferenceResolver.ts b/src/lib/converter/comments/declarationReferenceResolver.ts index 3e3a3c3a3..b36337c7b 100644 --- a/src/lib/converter/comments/declarationReferenceResolver.ts +++ b/src/lib/converter/comments/declarationReferenceResolver.ts @@ -2,6 +2,7 @@ import { ok } from "assert"; import { ContainerReflection, DeclarationReflection, + type DocumentReflection, type ProjectReflection, ReferenceReflection, type Reflection, @@ -225,10 +226,18 @@ function resolveSymbolReferencePart( let high: Reflection[] = []; let low: Reflection[] = []; - if ( - !(refl instanceof ContainerReflection) || - !refl.childrenIncludingDocuments - ) { + let children: + | ReadonlyArray + | undefined; + + if (refl instanceof ContainerReflection) { + children = refl.childrenIncludingDocuments; + } + if (!children && refl.isDeclaration() && refl.type?.type === "reflection") { + children = refl.type.declaration.childrenIncludingDocuments; + } + + if (!children) { return { high, low }; } @@ -238,12 +247,12 @@ function resolveSymbolReferencePart( // so that resolution doesn't behave very poorly with projects using JSDoc style resolution. // Also is more consistent with how TypeScript resolves link tags. case ".": - high = refl.childrenIncludingDocuments.filter( + high = children.filter( (r) => r.name === path.path && (r.kindOf(ReflectionKind.SomeExport) || r.flags.isStatic), ); - low = refl.childrenIncludingDocuments.filter( + low = children.filter( (r) => r.name === path.path && (!r.kindOf(ReflectionKind.SomeExport) || !r.flags.isStatic), @@ -254,7 +263,7 @@ function resolveSymbolReferencePart( // enum members, type literal properties case "#": high = - refl.children?.filter((r) => { + children?.filter((r) => { return ( r.name === path.path && r.kindOf(ReflectionKind.SomeMember) && @@ -269,7 +278,7 @@ function resolveSymbolReferencePart( if ( refl.kindOf(ReflectionKind.SomeModule | ReflectionKind.Project) ) { - high = refl.children?.filter((r) => r.name === path.path) || []; + high = children?.filter((r) => r.name === path.path) || []; } break; } diff --git a/src/lib/converter/comments/linkResolver.ts b/src/lib/converter/comments/linkResolver.ts index 855241e49..e2e082b34 100644 --- a/src/lib/converter/comments/linkResolver.ts +++ b/src/lib/converter/comments/linkResolver.ts @@ -5,6 +5,7 @@ import { DeclarationReflection, type InlineTagDisplayPart, Reflection, + ReflectionKind, ReflectionSymbolId, } from "../../models/index.js"; import { @@ -131,12 +132,20 @@ function resolveLinkTag( // Might already know where it should go if useTsLinkResolution is turned on if (part.target instanceof ReflectionSymbolId) { - const tsTarget = reflection.project.getReflectionFromSymbolId( + const tsTargets = reflection.project.getReflectionsFromSymbolId( part.target, ); - if (tsTarget) { - target = tsTarget; + if (tsTargets.length) { + // Find the target most likely to have a real url in the generated documentation + target = + tsTargets.find((r) => r.kindOf(ReflectionKind.SomeExport)) || + tsTargets.find( + (r) => + r.kindOf(ReflectionKind.SomeMember) && + r.parent?.kindOf(ReflectionKind.SomeExport), + ) || + tsTargets[0]; pos = end; defaultDisplayText = part.tsLinkText || diff --git a/src/lib/debug/debugReflectionLifetimes.ts b/src/lib/debug/debugReflectionLifetimes.ts new file mode 100644 index 000000000..6784efc8d --- /dev/null +++ b/src/lib/debug/debugReflectionLifetimes.ts @@ -0,0 +1,25 @@ +/* eslint-disable no-console */ +import type { Application } from "../application.js"; +import { ConverterEvents } from "../converter/converter-events.js"; +import type { Reflection } from "../models/index.js"; + +export function debugReflectionLifetimes(app: Application) { + app.converter.on(ConverterEvents.CREATE_PROJECT, logCreate); + app.converter.on(ConverterEvents.CREATE_SIGNATURE, logCreate); + app.converter.on(ConverterEvents.CREATE_TYPE_PARAMETER, logCreate); + app.converter.on(ConverterEvents.CREATE_DECLARATION, logCreate); + app.converter.on(ConverterEvents.CREATE_DOCUMENT, logCreate); + app.converter.on(ConverterEvents.CREATE_PARAMETER, logCreate); + + app.converter.on(ConverterEvents.CREATE_PROJECT, (_context, project) => { + const oldRemove = project["_removeReflection"]; + project["_removeReflection"] = function (reflection) { + console.log("Remove", reflection.id, reflection.getFullName()); + return oldRemove.call(this, reflection); + }; + }); +} + +function logCreate(_context: unknown, refl: Reflection) { + console.log("Create", refl.variant, refl.id, refl.getFullName()); +} diff --git a/src/lib/debug/debugRendererUrls.ts b/src/lib/debug/debugRendererUrls.ts new file mode 100644 index 000000000..8a02344cc --- /dev/null +++ b/src/lib/debug/debugRendererUrls.ts @@ -0,0 +1,88 @@ +/* eslint-disable no-console */ +import { join } from "node:path"; +import type { Application } from "../application.js"; +import { Reflection, type SomeReflection } from "../models/index.js"; +import type { SerializerComponent } from "../serialization/components.js"; +import type { JSONOutput } from "../serialization/index.js"; + +const serializer: SerializerComponent = { + priority: 0, + supports(x) { + return x instanceof Reflection; + }, + toObject(item, obj: any) { + obj.url = item.url; + obj.hasOwnDocument = item.hasOwnDocument; + // obj.anchor = item.anchor; + delete obj.sources; + delete obj.groups; + delete obj.categories; + delete obj.readme; + delete obj.content; + delete obj.kind; + delete obj.flags; + delete obj.defaultValue; + delete obj.symbolIdMap; + delete obj.files; + delete obj.packageName; + delete obj.variant; + delete obj.extendedTypes; + delete obj.inheritedFrom; + if (!["reflection", "reference"].includes(obj.type?.type)) { + delete obj.type; + } + + if (obj.comment) { + obj.comment.summary = obj.comment.summary.filter( + (part: JSONOutput.CommentDisplayPart) => + part.kind === "inline-tag", + ); + obj.comment.blockTags = obj.comment.blockTags?.filter( + (tag: JSONOutput.CommentTag) => { + tag.content = tag.content.filter( + (part) => part.kind === "inline-tag", + ); + return tag.content.length; + }, + ); + + if ( + !obj.comment.summary.length && + !obj.comment.blockTags?.length && + !obj.comment.modifierTags + ) { + delete obj.comment; + } + } + + return obj; + }, +}; + +export function debugRendererUrls( + app: Application, + { json = false, logs = false } = { logs: true }, +) { + app.renderer.postRenderAsyncJobs.push(async (evt) => { + if (json) { + app.serializer.addSerializer(serializer); + await app.generateJson( + evt.project, + join(evt.outputDirectory, "url_debug.json"), + ); + app.serializer.removeSerializer(serializer); + } + + if (logs) { + for (const id in evt.project.reflections) { + const refl = evt.project.reflections[id]; + console.log( + refl.id, + refl.getFullName(), + refl.url, + refl.hasOwnDocument, + ); + } + } + }); +} diff --git a/src/lib/debug/index.ts b/src/lib/debug/index.ts new file mode 100644 index 000000000..d8e3ba407 --- /dev/null +++ b/src/lib/debug/index.ts @@ -0,0 +1,2 @@ +export { debugRendererUrls } from "./debugRendererUrls.js"; +export { debugReflectionLifetimes } from "./debugReflectionLifetimes.js"; diff --git a/src/lib/internationalization/internationalization.ts b/src/lib/internationalization/internationalization.ts index 283101ca0..6c2b441c6 100644 --- a/src/lib/internationalization/internationalization.ts +++ b/src/lib/internationalization/internationalization.ts @@ -108,13 +108,13 @@ export class Internationalization { * Get the translation of the specified key, replacing placeholders * with the arguments specified. */ - translate( + translate( key: T, ...args: TranslatableStrings[T] ): TranslatedString { return ( this.allTranslations.get(this.application?.lang ?? "en").get(key) ?? - translatable[key] + translatable[key as keyof typeof translatable] ).replace(/\{(\d+)\}/g, (_, index) => { return args[+index] ?? "(no placeholder)"; }) as TranslatedString; diff --git a/src/lib/internationalization/locales/en.cts b/src/lib/internationalization/locales/en.cts index 41ad6ebfd..8b38479a6 100644 --- a/src/lib/internationalization/locales/en.cts +++ b/src/lib/internationalization/locales/en.cts @@ -95,6 +95,7 @@ export = { "The following symbols were marked as intentionally not exported, but were either not referenced in the documentation, or were exported:\n\t{0}", reflection_0_has_unused_mergeModuleWith_tag: "{0} has a @mergeModuleWith tag which could not be resolved", + reflection_0_links_to_1_with_text_2_but_resolved_to_3: `"{0}" links to "{1}" with text "{2}" which exists but does not have a link in the documentation, will link to "{3}" instead.`, // conversion plugins not_all_search_category_boosts_used_0: `Not all categories specified in searchCategoryBoosts were used in the documentation. The unused categories were:\n\t{0}`, diff --git a/src/lib/models/reflections/project.ts b/src/lib/models/reflections/project.ts index ea35ff21d..c5e9d5de9 100644 --- a/src/lib/models/reflections/project.ts +++ b/src/lib/models/reflections/project.ts @@ -391,6 +391,7 @@ export class ProjectReflection extends ContainerReflection { /** @internal */ registerSymbolId(reflection: Reflection, id: ReflectionSymbolId) { + this.removedSymbolIds.delete(id); this.reflectionIdToSymbolIdMap.set(reflection.id, id); const previous = this.symbolToReflectionIdMap.get(id); diff --git a/src/lib/models/types.ts b/src/lib/models/types.ts index e91a3c26e..2524356ba 100644 --- a/src/lib/models/types.ts +++ b/src/lib/models/types.ts @@ -833,7 +833,13 @@ export class ReferenceType extends Type { const resolved = resolvePotential.find((refl) => refl.kindOf(kind)) || resolvePotential.find((refl) => refl.kindOf(~kind))!; - this._target = resolved.id; + + // Do not mark the type as resolved at this point so that if it + // points to a member which is removed (e.g. by typedoc-plugin-zod) + // and then replaced it still ends up pointing at the right reflection. + // We will lock type reference resolution when serializing to JSON. + // this._target = resolved.id; + return resolved; } @@ -1002,6 +1008,8 @@ export class ReferenceType extends Type { target = this._target; } else if (this._project?.symbolIdHasBeenRemoved(this._target)) { target = -1; + } else if (this.reflection) { + target = this.reflection.id; } else { target = this._target.toObject(serializer); } @@ -1023,7 +1031,7 @@ export class ReferenceType extends Type { result.refersToTypeParameter = true; } - if (typeof this._target !== "number" && this.preferValues) { + if (typeof target !== "number" && this.preferValues) { result.preferValues = true; } diff --git a/src/lib/output/themes/MarkedPlugin.tsx b/src/lib/output/themes/MarkedPlugin.tsx index 9dd5e1b64..0289dfefd 100644 --- a/src/lib/output/themes/MarkedPlugin.tsx +++ b/src/lib/output/themes/MarkedPlugin.tsx @@ -4,7 +4,7 @@ import type md from "markdown-it" with { "resolution-mode": "require" }; import { ContextAwareRendererComponent } from "../components.js"; import { MarkdownEvent, RendererEvent, type PageEvent } from "../events.js"; -import { Option, renderElement, assertNever } from "../../utils/index.js"; +import { Option, renderElement, assertNever, type ValidationOptions } from "../../utils/index.js"; import { highlight, isLoadedLanguage, isSupportedLanguage } from "../../utils/highlighter.js"; import type { BundledTheme } from "@gerrit0/mini-shiki"; import { escapeHtml } from "../../utils/html.js"; @@ -35,6 +35,9 @@ export class MarkedPlugin extends ContextAwareRendererComponent { @Option("markdownLinkExternal") accessor markdownLinkExternal!: boolean; + @Option("validation") + accessor validation!: ValidationOptions; + private parser?: MarkdownIt; private renderedRelativeLinks: { @@ -141,21 +144,46 @@ export class MarkedPlugin extends ContextAwareRendererComponent { url = part.target; } else if ("id" in part.target) { // No point in trying to resolve a ReflectionSymbolId at this point, we've already - // tried and failed during the resolution step. + // tried and failed during the resolution step. Warnings related to those broken links + // have already been emitted. url = context.urlTo(part.target); kindClass = ReflectionKind.classString(part.target.kind); + + // If we don't have a URL the user probably linked to some deeply nested property + // which doesn't get an assigned URL. We'll walk upwards until we find a reflection + // which has a URL and link to that instead. + if (!url) { + // Walk upwards to find something we can link to. + let target = part.target.parent!; + url = context.urlTo(target); + while (!url && target.parent) { + target = target.parent; + // We know we'll always end up with a URL here eventually as the + // project always has a URL. + url = context.urlTo(target)!; + } + + if (this.validation.rewrittenLink) { + this.application.logger.warn( + this.application.i18n.reflection_0_links_to_1_with_text_2_but_resolved_to_3( + page.model.getFriendlyFullName(), + part.target.getFriendlyFullName(), + part.text, + target.getFriendlyFullName(), + ), + ); + } + } } if (useHtml) { const text = part.tag === "@linkcode" ? `${part.text}` : part.text; result.push( - url - ? `${text}` - : part.text, + `${text}`, ); } else { const text = part.tag === "@linkcode" ? "`" + part.text + "`" : part.text; - result.push(url ? `[${text}](${url})` : text); + result.push(`[${text}](${url})`); } } else { result.push(part.text); diff --git a/src/lib/output/themes/default/DefaultTheme.tsx b/src/lib/output/themes/default/DefaultTheme.tsx index 73020e458..c3fbac511 100644 --- a/src/lib/output/themes/default/DefaultTheme.tsx +++ b/src/lib/output/themes/default/DefaultTheme.tsx @@ -459,12 +459,30 @@ export class DefaultTheme extends Theme { return; } - if (!reflection.url || !DefaultTheme.URL_PREFIX.test(reflection.url)) { + // We support linking to reflections for types directly contained within an export + // but not any deeper. This is because TypeDoc may or may not render the type details + // for a property depending on whether or not it is deemed useful, and defining a link + // which might not be used may result in a link being generated which isn't valid. #2808. + // This should be kept in sync with the renderingChildIsUseful function. + if ( + reflection.kindOf(ReflectionKind.TypeLiteral) && + (!reflection.parent?.kindOf(ReflectionKind.SomeExport) || + (reflection.parent as DeclarationReflection).type?.type !== "reflection") + ) { + return; + } + + if ( + (!reflection.url || !DefaultTheme.URL_PREFIX.test(reflection.url)) && + !reflection.kindOf(ReflectionKind.TypeLiteral) + ) { let refl: Reflection | undefined = reflection; const parts = [refl.name]; while (refl.parent && refl.parent !== container && !(reflection.parent instanceof ProjectReflection)) { refl = refl.parent; // Avoid duplicate names for signatures + // BREAKING: In 0.28, also add !refl.kindOf(ReflectionKind.TypeLiteral) to this check to improve anchor + // generation by omitting useless __type prefixes. if (parts[0] !== refl.name) { parts.unshift(refl.name); } diff --git a/src/lib/output/themes/default/partials/member.signatures.tsx b/src/lib/output/themes/default/partials/member.signatures.tsx index 99e9bff3c..a19ad7b6b 100644 --- a/src/lib/output/themes/default/partials/member.signatures.tsx +++ b/src/lib/output/themes/default/partials/member.signatures.tsx @@ -10,7 +10,7 @@ export const memberSignatures = (context: DefaultThemeRenderContext, props: Decl {props.signatures?.map((item) => ( <> diff --git a/src/lib/output/themes/default/partials/typeDetails.tsx b/src/lib/output/themes/default/partials/typeDetails.tsx index bf92fe808..f6ae1036e 100644 --- a/src/lib/output/themes/default/partials/typeDetails.tsx +++ b/src/lib/output/themes/default/partials/typeDetails.tsx @@ -215,7 +215,7 @@ function renderChild(
{!!child.flags.isRest && ...} {child.name} - + {child.anchor && } {!!child.flags.isOptional && "?"}: function
@@ -245,7 +245,7 @@ function renderChild( {context.reflectionFlags(child)} {!!child.flags.isRest && ...} {child.name} - + {child.anchor && } {!!child.flags.isOptional && "?"} {": "} @@ -271,7 +271,7 @@ function renderChild( {context.reflectionFlags(child.getSignature)} get {child.name} - + {child.anchor && } (): {context.type(child.getSignature.type)} @@ -285,7 +285,7 @@ function renderChild( {context.reflectionFlags(child.setSignature)} set {child.name} - {!child.getSignature && } + {!child.getSignature && child.anchor && } ( {child.setSignature.parameters?.map((item) => ( <> @@ -329,6 +329,16 @@ function renderIndexSignature(context: DefaultThemeRenderContext, index: Signatu } function renderingChildIsUseful(refl: DeclarationReflection) { + // Object types directly under a variable/type alias will always be considered useful. + // This probably isn't ideal, but it is an easy thing to check when assigning URLs + // in the default theme, so we'll make the assumption that those properties ought to always + // be rendered. + // This should be kept in sync with the DefaultTheme.applyAnchorUrl function. + // We know refl.kind == TypeLiteral already here + if (refl.parent?.kindOf(ReflectionKind.SomeExport) && refl.type?.type === "reflection") { + return true; + } + if (renderingThisChildIsUseful(refl)) { return true; } diff --git a/src/lib/serialization/serializer.ts b/src/lib/serialization/serializer.ts index faf8f8260..d56f2203c 100644 --- a/src/lib/serialization/serializer.ts +++ b/src/lib/serialization/serializer.ts @@ -4,7 +4,7 @@ import type { ProjectReflection } from "../models/index.js"; import { SerializeEvent } from "./events.js"; import type { ModelToObject } from "./schema.js"; import type { SerializerComponent } from "./components.js"; -import { insertPrioritySorted } from "../utils/array.js"; +import { insertPrioritySorted, removeIfPresent } from "../utils/array.js"; export interface SerializerEvents { begin: [SerializeEvent]; @@ -46,6 +46,10 @@ export class Serializer extends EventDispatcher { insertPrioritySorted(this.serializers, serializer); } + removeSerializer(serializer: SerializerComponent): void { + removeIfPresent(this.serializers, serializer); + } + toObject }>( value: T, ): ModelToObject; diff --git a/src/lib/utils/options/declaration.ts b/src/lib/utils/options/declaration.ts index 8448523c4..a55b214bc 100644 --- a/src/lib/utils/options/declaration.ts +++ b/src/lib/utils/options/declaration.ts @@ -272,6 +272,10 @@ export type ValidationOptions = { * If set, TypeDoc will produce warnings about \{\@link\} tags which will produce broken links. */ invalidLink: boolean; + /** + * If set, TypeDoc will produce warnings about \{\@link\} tags which do not link directly to their target. + */ + rewrittenLink: boolean; /** * If set, TypeDoc will produce warnings about declarations that do not have doc comments */ diff --git a/src/lib/utils/options/sources/typedoc.ts b/src/lib/utils/options/sources/typedoc.ts index 2249c6802..d862771bb 100644 --- a/src/lib/utils/options/sources/typedoc.ts +++ b/src/lib/utils/options/sources/typedoc.ts @@ -977,6 +977,7 @@ export function addTypeDocOptions(options: Pick) { defaults: { notExported: true, invalidLink: true, + rewrittenLink: true, notDocumented: false, unusedMergeModuleWith: true, }, diff --git a/src/test/behavior.c2.test.ts b/src/test/behavior.c2.test.ts index 854247535..3ea97c7d6 100644 --- a/src/test/behavior.c2.test.ts +++ b/src/test/behavior.c2.test.ts @@ -907,6 +907,18 @@ describe("Behavior Tests", () => { equal(getLinkTexts(query(project, "scoped")), ["p"]); }); + it("Handles links to deeply nested members", () => { + const project = convert("linkResolution"); + // We detect a link to this deep property, but will warn about it actually linking + // to GH2808DeeplyNestedLink.prop when rendering. + equal(getLinks(query(project, "GH2808DeeplyNestedLink")), [ + [ + ReflectionKind.Property, + "GH2808DeeplyNestedLink.prop.__type.nested.__type.here", + ], + ]); + }); + it("Handles merged declarations", () => { const project = convert("mergedDeclarations"); const a = query(project, "SingleCommentMultiDeclaration"); diff --git a/src/test/converter2/behavior/linkResolution.ts b/src/test/converter2/behavior/linkResolution.ts index 1eafd0a82..731223a02 100644 --- a/src/test/converter2/behavior/linkResolution.ts +++ b/src/test/converter2/behavior/linkResolution.ts @@ -122,3 +122,17 @@ export namespace Navigation { foo = 456; } } + +/** + * {@link GH2808DeeplyNestedLink.prop.nested.here} + */ +export interface GH2808DeeplyNestedLink { + /** Prop docs */ + prop: { + /** Nested docs */ + nested: { + /** Here docs */ + here: true; + }; + }; +} diff --git a/src/test/utils/options/options.test.ts b/src/test/utils/options/options.test.ts index 8d9515f28..6172ad916 100644 --- a/src/test/utils/options/options.test.ts +++ b/src/test/utils/options/options.test.ts @@ -111,6 +111,7 @@ describe("Options", () => { notExported: true, notDocumented: true, invalidLink: true, + rewrittenLink: true, unusedMergeModuleWith: true, }); @@ -119,6 +120,7 @@ describe("Options", () => { notExported: false, notDocumented: false, invalidLink: false, + rewrittenLink: false, unusedMergeModuleWith: false, }); }); diff --git a/src/test/utils/options/readers/arguments.test.ts b/src/test/utils/options/readers/arguments.test.ts index 83b0dcdca..0b4a7db1a 100644 --- a/src/test/utils/options/readers/arguments.test.ts +++ b/src/test/utils/options/readers/arguments.test.ts @@ -159,6 +159,7 @@ describe("Options - ArgumentsReader", () => { notExported: true, notDocumented: false, invalidLink: true, + rewrittenLink: true, unusedMergeModuleWith: true, }); }, @@ -179,6 +180,7 @@ describe("Options - ArgumentsReader", () => { notExported: false, notDocumented: false, invalidLink: true, + rewrittenLink: true, unusedMergeModuleWith: true, }); }, @@ -194,6 +196,7 @@ describe("Options - ArgumentsReader", () => { notExported: true, notDocumented: true, invalidLink: true, + rewrittenLink: true, unusedMergeModuleWith: true, }); }, @@ -209,6 +212,7 @@ describe("Options - ArgumentsReader", () => { notExported: true, notDocumented: true, invalidLink: true, + rewrittenLink: true, unusedMergeModuleWith: true, }); }, @@ -224,6 +228,7 @@ describe("Options - ArgumentsReader", () => { notExported: false, notDocumented: false, invalidLink: false, + rewrittenLink: false, unusedMergeModuleWith: false, }); },