typedoc
Version:
Create api documentation for TypeScript projects.
281 lines (280 loc) • 11.6 kB
JavaScript
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;
}
}