UNPKG

typedoc

Version:

Create api documentation for TypeScript projects.

281 lines (280 loc) 11.6 kB
import { ok as assert } from "assert"; import ts from "typescript"; import { Comment, ContainerReflection, DeclarationReflection, ReferenceType, ReflectionFlag, ReflectionKind, } from "../models/index.js"; import { isNamedNode } from "./utils/nodes.js"; import { ConverterEvents } from "./converter-events.js"; import { resolveAliasedSymbol } from "./utils/symbols.js"; import { getComment, getFileComment, getJsDocComment, getNodeComment, getSignatureComment } from "./comments/index.js"; import { getHumanName, getQualifiedName } from "../utils/tsutils.js"; import { findPackageForPath, normalizePath } from "#node-utils"; import { createSymbolId } from "./factories/symbol-id.js"; import { removeIf } from "#utils"; /** * The context describes the current state the converter is in. */ export class Context { /** * The converter instance that has created the context. */ converter; /** * The TypeChecker instance returned by the TypeScript compiler. */ get checker() { return this.program.getTypeChecker(); } /** * The program currently being converted. * Accessing this property will throw if a source file is not currently being converted. */ get program() { assert(this._program, "Tried to access Context.program when not converting a source file"); return this._program; } _program; /** * All programs being converted. */ programs; /** * The project that is currently processed. */ project; /** * The scope or parent reflection that is currently processed. */ scope; convertingTypeNode = false; // Inherited by withScope convertingClassOrInterface = false; // Not inherited shouldBeStatic = false; // Not inherited inlineType = new Set(); // Inherited by withScope preventInline = new Set(); // Inherited by withScope reflectionIdToSymbolMap = new Map(); /** * Create a new Context instance. * * @param converter The converter instance that has created the context. * @internal */ constructor(converter, programs, project, scope = project) { this.converter = converter; this.programs = programs; this.project = project; this.scope = scope; } /** @internal */ get logger() { return this.converter.application.logger; } /** * Return the type declaration of the given node. * * @param node The TypeScript node whose type should be resolved. * @returns The type declaration of the given node. */ getTypeAtLocation(node) { let nodeType; try { nodeType = this.checker.getTypeAtLocation(node); } catch { // ignore } if (!nodeType) { if (node.symbol) { nodeType = this.checker.getDeclaredTypeOfSymbol(node.symbol); // The TS types lie due to ts.SourceFile } else if (node.parent?.symbol) { nodeType = this.checker.getDeclaredTypeOfSymbol(node.parent.symbol); // The TS types lie due to ts.SourceFile } else if (node.parent?.parent?.symbol) { nodeType = this.checker.getDeclaredTypeOfSymbol(node.parent.parent.symbol); } } return nodeType; } getSymbolAtLocation(node) { let symbol = this.checker.getSymbolAtLocation(node); if (!symbol && isNamedNode(node)) { symbol = this.checker.getSymbolAtLocation(node.name); } return symbol; } expectSymbolAtLocation(node) { const symbol = this.getSymbolAtLocation(node); if (!symbol) { const { line } = ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.pos); throw new Error(`Expected a symbol for node with kind ${ts.SyntaxKind[node.kind]} at ${node.getSourceFile().fileName}:${line + 1}`); } return symbol; } resolveAliasedSymbol(symbol) { return resolveAliasedSymbol(symbol, this.checker); } createDeclarationReflection(kind, symbol, exportSymbol, // We need this because modules don't always have symbols. nameOverride) { const name = getHumanName(nameOverride ?? exportSymbol?.name ?? symbol?.name ?? "unknown"); if (this.convertingClassOrInterface) { if (kind === ReflectionKind.Function) { kind = ReflectionKind.Method; } if (kind === ReflectionKind.Variable) { kind = ReflectionKind.Property; } } const reflection = new DeclarationReflection(name, kind, this.scope); this.postReflectionCreation(reflection, symbol, exportSymbol); return reflection; } postReflectionCreation(reflection, symbol, exportSymbol) { if (exportSymbol && reflection.kind & (ReflectionKind.SomeModule | ReflectionKind.Reference)) { reflection.comment = this.getComment(exportSymbol, reflection.kind); } if (symbol && !reflection.comment) { reflection.comment = this.getComment(symbol, reflection.kind); } if (this.shouldBeStatic) { reflection.setFlag(ReflectionFlag.Static); } if (reflection instanceof DeclarationReflection) { reflection.escapedName = symbol?.escapedName ? String(symbol.escapedName) : undefined; this.addChild(reflection); } if (symbol && this.converter.isExternal(symbol, this.checker)) { reflection.setFlag(ReflectionFlag.External); } if (exportSymbol) { this.registerReflection(reflection, exportSymbol, void 0); } const path = reflection.kindOf(ReflectionKind.Namespace | ReflectionKind.Module) ? symbol?.declarations?.find(ts.isSourceFile)?.fileName : undefined; if (path) { this.registerReflection(reflection, symbol, normalizePath(path)); } else { this.registerReflection(reflection, symbol, undefined); } } finalizeDeclarationReflection(reflection) { this.converter.trigger(ConverterEvents.CREATE_DECLARATION, this, reflection); if (reflection.kindOf(ReflectionKind.MayContainDocuments)) { this.converter.processDocumentTags(reflection, reflection); } } /** * Create a {@link ReferenceType} which points to the provided symbol. * * @privateRemarks * This is available on Context so that it can be monkey-patched by typedoc-plugin-missing-exports */ createSymbolReference(symbol, context, name) { const ref = ReferenceType.createUnresolvedReference(name ?? symbol.name, createSymbolId(symbol), context.project, getQualifiedName(symbol, name ?? symbol.name)); ref.refersToTypeParameter = !!(symbol.flags & ts.SymbolFlags.TypeParameter); const symbolPath = symbol.declarations?.[0]?.getSourceFile().fileName; if (!symbolPath) return ref; ref.package = findPackageForPath(symbolPath)?.[0]; return ref; } addChild(reflection) { if (this.scope instanceof ContainerReflection) { this.scope.addChild(reflection); } } shouldIgnore(symbol) { return this.converter.shouldIgnore(symbol, this.checker); } /** * Register a newly generated reflection. All created reflections should be * passed to this method to ensure that the project helper functions work correctly. * * @param reflection The reflection that should be registered. * @param symbol The symbol the given reflection was resolved from. */ registerReflection(reflection, symbol, filePath) { if (symbol) { this.reflectionIdToSymbolMap.set(reflection.id, symbol); const id = createSymbolId(symbol); // #2466 // If we just registered a member of a class or interface, then we need to check if // we've registered this symbol before under the wrong parent reflection. // This can happen because the compiler API will use non-dependently-typed symbols // for properties of classes/interfaces which inherit them, so we can't rely on the // property being unique for each class. if (reflection.parent?.kindOf(ReflectionKind.ClassOrInterface) && reflection.kindOf(ReflectionKind.SomeMember)) { const saved = this.project["symbolToReflectionIdMap"].get(id); const parentSymbolReflection = symbol.parent && this.getReflectionFromSymbol(symbol.parent); if (typeof saved === "object" && saved.length > 1 && parentSymbolReflection) { removeIf(saved, (item) => this.project.getReflectionById(item)?.parent !== parentSymbolReflection); } } this.project.registerReflection(reflection, id, filePath); } else { this.project.registerReflection(reflection, void 0, filePath); } } getReflectionFromSymbol(symbol) { return this.project.getReflectionFromSymbolId(createSymbolId(symbol)); } getSymbolFromReflection(reflection) { return this.reflectionIdToSymbolMap.get(reflection.id); } /** @internal */ setActiveProgram(program) { this._program = program; } getComment(symbol, kind) { return getComment(symbol, kind, this.converter.config, this.logger, this.checker, this.project.files); } getNodeComment(node, moduleComment) { return getNodeComment(node, moduleComment, this.converter.config, this.logger, this.checker, this.project.files); } getFileComment(node) { return getFileComment(node, this.converter.config, this.logger, this.checker, this.project.files); } getJsDocComment(declaration) { return getJsDocComment(declaration, this.converter.config, this.logger, this.checker, this.project.files); } getSignatureComment(declaration) { return getSignatureComment(declaration, this.converter.config, this.logger, this.checker, this.project.files); } shouldInline(symbol, name) { if (this.preventInline.has(name)) return false; if (this.inlineType.has(name)) return true; return this .getComment(symbol, ReflectionKind.Interface) ?.hasModifier("@inline") ?? false; } withScope(scope) { assert(scope.parent === this.scope || scope === this.scope, "Incorrect context used for withScope"); const context = new Context(this.converter, this.programs, this.project, scope); context.convertingTypeNode = this.convertingTypeNode; context.setActiveProgram(this._program); context.reflectionIdToSymbolMap = this.reflectionIdToSymbolMap; context.preventInline = new Set(this.preventInline); context.inlineType = new Set(this.inlineType); for (const tag of scope.comment?.blockTags || []) { if (tag.tag === "@preventInline") { context.preventInline.add(Comment.combineDisplayParts(tag.content)); } else if (tag.tag === "@inlineType") { context.inlineType.add(Comment.combineDisplayParts(tag.content)); } } return context; } }