diff --git a/CHANGELOG.md b/CHANGELOG.md index 481171619..1b65f7007 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,13 @@ # Unreleased +### Features + +- `ReferenceType`s which reference an external symbol will now include `qualifiedName` and `package` in their serialized JSON. + ### Bug Fixes - Fixed line height of `h1` and `h2` elements being too low, #1796. +- Symbol names passed to `addUnknownSymbolResolver` will now be correctly given the qualified name to the symbol being referenced. ## v0.22.10 (2021-11-25) diff --git a/src/lib/converter/plugins/DecoratorPlugin.ts b/src/lib/converter/plugins/DecoratorPlugin.ts index ef8570fc8..fb8cd8519 100644 --- a/src/lib/converter/plugins/DecoratorPlugin.ts +++ b/src/lib/converter/plugins/DecoratorPlugin.ts @@ -86,10 +86,10 @@ export class DecoratorPlugin extends ConverterComponent { const type = context.checker.getTypeAtLocation(identifier); if (type && type.symbol) { - info.type = new ReferenceType( - info.name, + info.type = ReferenceType.createSymbolReference( context.resolveAliasedSymbol(type.symbol), - context.project + context, + info.name ); if (callExpression && callExpression.arguments) { @@ -105,7 +105,7 @@ export class DecoratorPlugin extends ConverterComponent { const usages = this.usages.get(type.symbol) ?? []; usages.push( - new ReferenceType( + ReferenceType.createResolvedReference( reflection.name, reflection, context.project diff --git a/src/lib/converter/plugins/ImplementsPlugin.ts b/src/lib/converter/plugins/ImplementsPlugin.ts index 9f0fb88f4..e8ce3984d 100644 --- a/src/lib/converter/plugins/ImplementsPlugin.ts +++ b/src/lib/converter/plugins/ImplementsPlugin.ts @@ -86,11 +86,12 @@ export class ImplementsPlugin extends ConverterComponent { const interfaceMemberName = interfaceReflection.name + "." + interfaceMember.name; - classMember.implementationOf = new ReferenceType( - interfaceMemberName, - interfaceMember, - context.project - ); + classMember.implementationOf = + ReferenceType.createResolvedReference( + interfaceMemberName, + interfaceMember, + context.project + ); copyComment(classMember, interfaceMember); if ( @@ -119,11 +120,12 @@ export class ImplementsPlugin extends ConverterComponent { interfaceMember.signatures )) { if (clsSig.implementationOf) { - clsSig.implementationOf = new ReferenceType( - clsSig.implementationOf.name, - intSig, - context.project - ); + clsSig.implementationOf = + ReferenceType.createResolvedReference( + clsSig.implementationOf.name, + intSig, + context.project + ); } copyComment(clsSig, intSig); } @@ -164,7 +166,7 @@ export class ImplementsPlugin extends ConverterComponent { child.signatures ?? [], parentMember.signatures ?? [] )) { - childSig[key] = new ReferenceType( + childSig[key] = ReferenceType.createResolvedReference( `${parent.name}.${parentMember.name}`, parentSig, context.project @@ -172,7 +174,7 @@ export class ImplementsPlugin extends ConverterComponent { copyComment(childSig, parentSig); } - child[key] = new ReferenceType( + child[key] = ReferenceType.createResolvedReference( `${parent.name}.${parentMember.name}`, parentMember, context.project diff --git a/src/lib/converter/plugins/TypePlugin.ts b/src/lib/converter/plugins/TypePlugin.ts index ef216e91c..3e720c0f8 100644 --- a/src/lib/converter/plugins/TypePlugin.ts +++ b/src/lib/converter/plugins/TypePlugin.ts @@ -42,7 +42,7 @@ export class TypePlugin extends ConverterComponent { target.implementedBy = []; } target.implementedBy.push( - new ReferenceType( + ReferenceType.createResolvedReference( reflection.name, reflection, context.project @@ -56,7 +56,7 @@ export class TypePlugin extends ConverterComponent { target.extendedBy = []; } target.extendedBy.push( - new ReferenceType( + ReferenceType.createResolvedReference( reflection.name, reflection, context.project @@ -122,7 +122,11 @@ export class TypePlugin extends ConverterComponent { } push([ - new ReferenceType(reflection.name, reflection, context.project), + ReferenceType.createResolvedReference( + reflection.name, + reflection, + context.project + ), ]); hierarchy.isTarget = true; diff --git a/src/lib/converter/types.ts b/src/lib/converter/types.ts index 59fba9612..97bd0cf43 100644 --- a/src/lib/converter/types.ts +++ b/src/lib/converter/types.ts @@ -281,10 +281,9 @@ const exprWithTypeArgsConverter: TypeConverter< } const parameters = node.typeArguments?.map((type) => convertType(context, type)) ?? []; - const ref = new ReferenceType( - targetSymbol.name, + const ref = ReferenceType.createSymbolReference( context.resolveAliasedSymbol(targetSymbol), - context.project + context ); ref.typeArguments = parameters; return ref; @@ -362,19 +361,19 @@ const importType: TypeConverter = { const name = node.qualifier?.getText() ?? "__module"; const symbol = context.checker.getSymbolAtLocation(node); assert(symbol, "Missing symbol when converting import type node"); - return new ReferenceType( - name, + return ReferenceType.createSymbolReference( context.resolveAliasedSymbol(symbol), - context.project + context, + name ); }, convertType(context, type) { const symbol = type.getSymbol(); assert(symbol, "Missing symbol when converting import type"); // Should be a compiler error - return new ReferenceType( - "__module", + return ReferenceType.createSymbolReference( context.resolveAliasedSymbol(symbol), - context.project + context, + "__module" ); }, }; @@ -584,10 +583,10 @@ const queryConverter: TypeConverter = { } return new QueryType( - new ReferenceType( - node.exprName.getText(), + ReferenceType.createSymbolReference( context.resolveAliasedSymbol(querySymbol), - context.project + context, + node.exprName.getText() ) ); }, @@ -600,10 +599,9 @@ const queryConverter: TypeConverter = { )}. This is a bug.` ); return new QueryType( - new ReferenceType( - symbol.name, + ReferenceType.createSymbolReference( context.resolveAliasedSymbol(symbol), - context.project + context ) ); }, @@ -630,10 +628,10 @@ const referenceConverter: TypeConverter< const name = node.typeName.getText(); - const type = new ReferenceType( - name, + const type = ReferenceType.createSymbolReference( context.resolveAliasedSymbol(symbol), - context.project + context, + name ); type.typeArguments = node.typeArguments?.map((type) => convertType(context, type) @@ -651,10 +649,9 @@ const referenceConverter: TypeConverter< ); } - const ref = new ReferenceType( - symbol.name, + const ref = ReferenceType.createSymbolReference( context.resolveAliasedSymbol(symbol), - context.project + context ); ref.typeArguments = ( type.aliasSymbol ? type.aliasTypeArguments : type.typeArguments diff --git a/src/lib/models/types.ts b/src/lib/models/types.ts index dc71dca16..376d1b7fc 100644 --- a/src/lib/models/types.ts +++ b/src/lib/models/types.ts @@ -1,4 +1,5 @@ import type * as ts from "typescript"; +import type { Context } from "../converter"; import { Reflection } from "./reflections/abstract"; import type { DeclarationReflection } from "./reflections/declaration"; import type { ProjectReflection } from "./reflections/project"; @@ -512,6 +513,12 @@ export class ReferenceType extends Type { return resolved; } + /** + * Don't use this if at all possible. It will eventually go away since models may not + * retain information from the original TS objects to enable documentation generation from + * previously generated JSON. + * @internal + */ getSymbol(): ts.Symbol | undefined { if (typeof this._target === "number") { return; @@ -519,10 +526,23 @@ export class ReferenceType extends Type { return this._target; } + /** + * The fully qualified name of the referenced type, relative to the file it is defined in. + * This will usually be the same as `name`, unless namespaces are used. + * Will only be set for `ReferenceType`s pointing to a symbol within `node_modules`. + */ + qualifiedName?: string; + + /** + * The package that this type is referencing. + * Will only be set for `ReferenceType`s pointing to a symbol within `node_modules`. + */ + package?: string; + private _target: ts.Symbol | number; private _project: ProjectReflection | null; - constructor( + private constructor( name: string, target: ts.Symbol | Reflection | number, project: ProjectReflection | null @@ -533,6 +553,58 @@ export class ReferenceType extends Type { this._project = project; } + static createResolvedReference( + name: string, + target: Reflection | number, + project: ProjectReflection | null + ) { + return new ReferenceType(name, target, project); + } + + static createSymbolReference( + symbol: ts.Symbol, + context: Context, + name?: string + ) { + const ref = new ReferenceType( + name ?? symbol.name, + symbol, + context.project + ); + + const symbolPath = symbol?.declarations?.[0] + ?.getSourceFile() + .fileName.replace(/\\/g, "/"); + if (!symbolPath) return ref; + + let startIndex = symbolPath.indexOf("node_modules/"); + if (startIndex === -1) return ref; + startIndex += "node_modules/".length; + let stopIndex = symbolPath.indexOf("/", startIndex); + // Scoped package, e.g. `@types/node` + if (symbolPath[startIndex] === "@") { + stopIndex = symbolPath.indexOf("/", stopIndex + 1); + } + + const packageName = symbolPath.substring(startIndex, stopIndex); + ref.package = packageName; + + const qualifiedName = context.checker.getFullyQualifiedName(symbol); + // I think this is less bad than depending on symbol.parent... + // https://github.com/microsoft/TypeScript/issues/38344 + // It will break if someone names a directory with a quote in it, but so will lots + // of other things including other parts of TypeDoc. Until it *actually* breaks someone... + if (qualifiedName.startsWith('"')) { + ref.qualifiedName = qualifiedName.substring( + qualifiedName.indexOf('".', 1) + 2 + ); + } else { + ref.qualifiedName = qualifiedName; + } + + return ref; + } + /** @internal this is used for type parameters, which don't actually point to something */ static createBrokenReference(name: string, project: ProjectReflection) { return new ReferenceType(name, -1, project); diff --git a/src/lib/output/renderer.ts b/src/lib/output/renderer.ts index 6235dfc09..911e1dd45 100644 --- a/src/lib/output/renderer.ts +++ b/src/lib/output/renderer.ts @@ -6,7 +6,6 @@ * series of {@link RendererEvent} events. Instances of {@link BasePlugin} can listen to these events and * alter the generated output. */ -import type * as ts from "typescript"; import * as fs from "fs"; import * as path from "path"; @@ -22,7 +21,7 @@ import { Component, ChildableComponent } from "../utils/component"; import { BindOption, EventHooks } from "../utils"; import { loadHighlighter } from "../utils/highlighter"; import type { Theme as ShikiTheme } from "shiki"; -import { Reflection } from "../models"; +import { ReferenceType, Reflection } from "../models"; import type { JsxElement } from "../utils/jsx.elements"; import type { DefaultThemeRenderContext } from "./themes/default/DefaultThemeRenderContext"; @@ -194,31 +193,15 @@ export class Renderer extends ChildableComponent< * symbols so that we don't need to keep the program around forever. * @internal */ - attemptExternalResolution( - symbol: ts.Symbol | undefined - ): string | undefined { - const symbolPath = symbol?.declarations?.[0] - ?.getSourceFile() - .fileName.replace(/\\/g, "/"); - if (!symbolPath) { + attemptExternalResolution(type: ReferenceType): string | undefined { + if (!type.qualifiedName || !type.package) { return; } - let startIndex = symbolPath.indexOf("node_modules/"); - if (startIndex === -1) { - return; - } - startIndex += "node_modules/".length; - let stopIndex = symbolPath.indexOf("/", startIndex); - // Scoped package, e.g. `@types/node` - if (symbolPath[startIndex] === "@") { - stopIndex = symbolPath.indexOf("/", stopIndex + 1); - } - const packageName = symbolPath.substring(startIndex, stopIndex); - const resolvers = this.unknownSymbolResolvers.get(packageName); + const resolvers = this.unknownSymbolResolvers.get(type.package); for (const resolver of resolvers || []) { - const resolved = resolver(symbol!.name); + const resolved = resolver(type.qualifiedName); if (resolved) return resolved; } } diff --git a/src/lib/output/themes/default/DefaultThemeRenderContext.ts b/src/lib/output/themes/default/DefaultThemeRenderContext.ts index e600006ff..b7e07c321 100644 --- a/src/lib/output/themes/default/DefaultThemeRenderContext.ts +++ b/src/lib/output/themes/default/DefaultThemeRenderContext.ts @@ -1,6 +1,5 @@ -import type * as ts from "typescript"; import type { RendererHooks } from "../.."; -import type { Reflection } from "../../../models"; +import type { ReferenceType, Reflection } from "../../../models"; import type { Options } from "../../../utils"; import type { DefaultTheme } from "./DefaultTheme"; import { defaultLayout } from "./layouts/default"; @@ -54,8 +53,8 @@ export class DefaultThemeRenderContext { return md ? this.theme.markedPlugin.parseMarkdown(md) : ""; }; - attemptExternalResolution = (symbol: ts.Symbol | undefined) => { - return this.theme.owner.attemptExternalResolution(symbol); + attemptExternalResolution = (type: ReferenceType) => { + return this.theme.owner.attemptExternalResolution(type); }; reflectionTemplate = bind(reflectionTemplate, this); diff --git a/src/lib/output/themes/default/partials/type.tsx b/src/lib/output/themes/default/partials/type.tsx index d5bff1d6d..850d60bf2 100644 --- a/src/lib/output/themes/default/partials/type.tsx +++ b/src/lib/output/themes/default/partials/type.tsx @@ -255,7 +255,7 @@ const typeRenderers: { name = renderUniquePath(context, reflection); } } else { - const externalUrl = context.attemptExternalResolution(type.getSymbol()); + const externalUrl = context.attemptExternalResolution(type); if (externalUrl) { name = ( diff --git a/src/lib/serialization/schema.ts b/src/lib/serialization/schema.ts index a8753ce94..0d9f03de5 100644 --- a/src/lib/serialization/schema.ts +++ b/src/lib/serialization/schema.ts @@ -270,7 +270,10 @@ export interface QueryType extends Type, S {} export interface ReferenceType extends Type, - S { + S< + M.ReferenceType, + "type" | "name" | "typeArguments" | "qualifiedName" | "package" + > { id?: number; } diff --git a/src/lib/serialization/serializers/types/reference.ts b/src/lib/serialization/serializers/types/reference.ts index 9f90918d6..82b3c1340 100644 --- a/src/lib/serialization/serializers/types/reference.ts +++ b/src/lib/serialization/serializers/types/reference.ts @@ -22,6 +22,11 @@ export class ReferenceTypeSerializer extends TypeSerializerComponent { it("Wraps type queries", () => { const type = new T.OptionalType( - new T.QueryType(new T.ReferenceType("X", -1, null)) + new T.QueryType( + T.ReferenceType.createResolvedReference("X", -1, null) + ) ); equal(type.toString(), "(typeof X)?"); }); @@ -266,7 +268,9 @@ describe("Type.toString", () => { describe("Type operator", () => { it("Does not wrap type query", () => { const type = new T.TypeOperatorType( - new T.QueryType(new T.ReferenceType("X", -1, null)), + new T.QueryType( + T.ReferenceType.createResolvedReference("X", -1, null) + ), "keyof" ); equal(type.toString(), "keyof typeof X");