typedoc
Version:
Create api documentation for TypeScript projects.
377 lines (376 loc) • 15.6 kB
JavaScript
import { TraverseProperty } from "./abstract.js";
import { ContainerReflection } from "./container.js";
import { ReferenceReflection } from "./reference.js";
import { IntrinsicType, makeRecursiveVisitor } from "../types.js";
import { assertNever, removeIf, removeIfPresent } from "../../utils/index.js";
import { ReflectionKind } from "./kind.js";
import { Comment } from "../comments/index.js";
import { ReflectionSymbolId } from "./ReflectionSymbolId.js";
import { DefaultMap, StableKeyMap } from "../../utils/map.js";
/**
* A reflection that represents the root of the project.
*
* The project reflection acts as a global index, one may receive all reflections
* and source files of the processed project through this reflection.
* @category Reflections
*/
export class ProjectReflection extends ContainerReflection {
variant = "project";
// Used to resolve references.
symbolToReflectionIdMap = new StableKeyMap();
reflectionIdToSymbolIdMap = new Map();
reflectionIdToSymbolMap = new Map();
removedSymbolIds = new StableKeyMap();
// Maps a reflection ID to all references eventually referring to it.
referenceGraph;
// Maps a reflection ID to all reflections with it as their parent.
reflectionChildren = new DefaultMap(() => []);
/**
* A list of all reflections within the project. DO NOT MUTATE THIS OBJECT.
* All mutation should be done via {@link registerReflection} and {@link removeReflection}
* to ensure that links to reflections remain valid.
*
* This may be replaced with a `Map<number, Reflection>` someday.
*/
reflections = { [this.id]: this };
/**
* The name of the package that this reflection documents according to package.json.
*/
packageName;
/**
* The version of the package that this reflection documents according to package.json.
*/
packageVersion;
/**
* The contents of the readme.md file of the project when found.
*/
readme;
/**
* Object which describes where to find content for relative links.
*/
files;
constructor(name, registry) {
super(name, ReflectionKind.Project);
this.reflections[this.id] = this;
this.files = registry;
}
/**
* Return whether this reflection is the root / project reflection.
*/
isProject() {
return true;
}
/**
* Return a list of all reflections in this project of a certain kind.
*
* @param kind The desired kind of reflection.
* @returns An array containing all reflections with the desired kind.
*/
getReflectionsByKind(kind) {
return Object.values(this.reflections).filter((reflection) => reflection.kindOf(kind));
}
/**
* Registers the given reflection so that it can be quickly looked up by helper methods.
* Should be called for *every* reflection added to the project.
*/
registerReflection(reflection, symbol, filePath) {
this.referenceGraph = undefined;
if (reflection.parent) {
this.reflectionChildren
.get(reflection.parent.id)
.push(reflection.id);
}
this.reflections[reflection.id] = reflection;
if (symbol) {
this.reflectionIdToSymbolMap.set(reflection.id, symbol);
const id = new ReflectionSymbolId(symbol);
this.registerSymbolId(reflection, id);
// #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.symbolToReflectionIdMap.get(id);
const parentSymbolReflection = symbol.parent &&
this.getReflectionFromSymbol(symbol.parent);
if (typeof saved === "object" &&
saved.length > 1 &&
parentSymbolReflection) {
removeIf(saved, (item) => this.getReflectionById(item)?.parent !==
parentSymbolReflection);
}
}
}
if (filePath) {
this.files.registerReflection(filePath, reflection);
}
}
/**
* Removes references to reflections contained within the provided type.
* Plugins which overwrite types on reflections should pass the type to this
* method before overwriting the property.
* @since 0.26.6
*/
removeTypeReflections(type) {
type?.visit(makeRecursiveVisitor({
reflection: (type) => {
this.removeReflection(type.declaration);
},
}));
}
/**
* Removes a reflection from the documentation. Can be used by plugins to filter reflections
* out of the generated documentation. Has no effect if the reflection is not present in the
* project.
*/
removeReflection(reflection) {
// Remove the reflection...
this._removeReflection(reflection);
// And now try to remove references to it in the parent reflection.
// This might not find anything if someone called removeReflection on a member of a union
// but I think that could only be caused by a plugin doing something weird, not by a regular
// user... so this is probably good enough for now. Reflections that live on types are
// kind of half-real anyways.
const parent = reflection.parent;
parent?.traverse((child, property) => {
if (child !== reflection) {
return true; // Continue iteration
}
switch (property) {
case TraverseProperty.Children:
case TraverseProperty.Documents:
parent.removeChild(reflection);
break;
case TraverseProperty.GetSignature:
delete parent.getSignature;
break;
case TraverseProperty.IndexSignature:
removeIfPresent(parent.indexSignatures, reflection);
if (!parent.indexSignatures?.length) {
delete parent.indexSignatures;
}
break;
case TraverseProperty.Parameters:
removeIfPresent(reflection.parent.parameters, reflection);
if (!reflection.parent.parameters
?.length) {
delete reflection.parent
.parameters;
}
break;
case TraverseProperty.SetSignature:
delete parent.setSignature;
break;
case TraverseProperty.Signatures:
removeIfPresent(parent.signatures, reflection);
if (!parent.signatures?.length) {
delete parent.signatures;
}
break;
case TraverseProperty.TypeLiteral:
parent.type = new IntrinsicType("Object");
break;
case TraverseProperty.TypeParameter:
removeIfPresent(parent.typeParameters, reflection);
if (!parent.typeParameters?.length) {
delete parent.typeParameters;
}
break;
default:
assertNever(property);
}
return false; // Stop iteration
});
}
/** @internal */
mergeModules(source, target) {
// First, tell the children about their new parent
delete this.referenceGraph;
const oldChildrenIds = this.reflectionChildren.getNoInsert(source.id) || [];
const newChildren = this.reflectionChildren.get(target.id);
for (const childId of oldChildrenIds) {
const childRefl = this.getReflectionById(childId);
// To avoid conflicting with some plugins which do this surgery somewhat incorrectly
// (typedoc-plugin-merge-modules and likely others I'm not aware of) only move children
// which are still children
if (childRefl?.parent === source) {
childRefl.parent = target;
newChildren.push(childId);
target.addChild(childRefl);
}
}
// Then remove the now-empty parent
this.reflectionChildren.delete(source.id);
this.removeReflection(source);
// And remove any outdated collections of children on the new parent.
// So long as this is used before REVIVE(-100) or EVENT_BEGIN_RESOLVE(-100)
// this will make the appropriate plugin rebuild the lists.
delete target.groups;
delete target.categories;
}
/**
* Remove a reflection without updating the parent reflection to remove references to the removed reflection.
*/
_removeReflection(reflection) {
this.files.removeReflection(reflection);
// Remove references pointing to this reflection
const graph = this.getReferenceGraph();
for (const id of graph.get(reflection.id) ?? []) {
const ref = this.getReflectionById(id);
if (ref) {
this.removeReflection(ref);
}
}
graph.delete(reflection.id);
// Remove children of this reflection
for (const childId of this.reflectionChildren.getNoInsert(reflection.id) || []) {
const child = this.getReflectionById(childId);
// Only remove if the child's parent is still actually this reflection.
// This might not be the case if a plugin has moved this reflection to another parent.
// (typedoc-plugin-merge-modules)
if (child?.parent === reflection) {
this._removeReflection(child);
}
}
this.reflectionChildren.delete(reflection.id);
// Remove references from the TS symbol to this reflection.
const symbolId = this.reflectionIdToSymbolIdMap.get(reflection.id);
if (symbolId) {
const saved = this.symbolToReflectionIdMap.get(symbolId);
if (saved === reflection.id) {
this.symbolToReflectionIdMap.delete(symbolId);
this.removedSymbolIds.set(symbolId, true);
}
else if (typeof saved === "object") {
removeIfPresent(saved, reflection.id);
if (saved.length === 0) {
this.removedSymbolIds.set(symbolId, true);
}
}
}
this.reflectionIdToSymbolMap.delete(reflection.id);
this.reflectionIdToSymbolIdMap.delete(reflection.id);
delete this.reflections[reflection.id];
}
/**
* Gets the reflection registered for the given reflection ID, or undefined if it is not present
* in the project.
*/
getReflectionById(id) {
return this.reflections[id];
}
/**
* Gets the reflection associated with the given symbol, if it exists.
* @internal
*/
getReflectionFromSymbol(symbol) {
return this.getReflectionFromSymbolId(new ReflectionSymbolId(symbol));
}
/**
* Gets the reflection associated with the given symbol id, if it exists.
* If there are multiple reflections associated with this symbol, gets the first one.
* @internal
*/
getReflectionFromSymbolId(symbolId) {
return this.getReflectionsFromSymbolId(symbolId)[0];
}
/** @internal */
getReflectionsFromSymbolId(symbolId) {
const id = this.symbolToReflectionIdMap.get(symbolId);
if (typeof id === "number") {
return [this.getReflectionById(id)];
}
else if (typeof id === "object") {
return id.map((id) => this.getReflectionById(id));
}
return [];
}
/** @internal */
getSymbolIdFromReflection(reflection) {
return this.reflectionIdToSymbolIdMap.get(reflection.id);
}
/** @internal */
registerSymbolId(reflection, id) {
this.removedSymbolIds.delete(id);
this.reflectionIdToSymbolIdMap.set(reflection.id, id);
const previous = this.symbolToReflectionIdMap.get(id);
if (previous) {
if (typeof previous === "number") {
this.symbolToReflectionIdMap.set(id, [previous, reflection.id]);
}
else {
previous.push(reflection.id);
}
}
else {
this.symbolToReflectionIdMap.set(id, reflection.id);
}
}
symbolIdHasBeenRemoved(id) {
return this.removedSymbolIds.has(id);
}
/**
* THIS MAY NOT BE USED AFTER CONVERSION HAS FINISHED.
* @internal
*/
getSymbolFromReflection(reflection) {
return this.reflectionIdToSymbolMap.get(reflection.id);
}
getReferenceGraph() {
if (!this.referenceGraph) {
this.referenceGraph = new Map();
for (const id in this.reflections) {
const ref = this.reflections[id];
if (ref instanceof ReferenceReflection) {
const target = ref.tryGetTargetReflection();
if (target) {
const refs = this.referenceGraph.get(target.id) ?? [];
refs.push(ref.id);
this.referenceGraph.set(target.id, refs);
}
}
}
}
return this.referenceGraph;
}
toObject(serializer) {
const symbolIdMap = {};
this.reflectionIdToSymbolIdMap.forEach((sid, id) => {
symbolIdMap[id] = sid.toObject(serializer);
});
return {
...super.toObject(serializer),
variant: this.variant,
packageName: this.packageName,
packageVersion: this.packageVersion,
readme: Comment.serializeDisplayParts(serializer, this.readme),
symbolIdMap,
files: serializer.toObject(this.files),
};
}
fromObject(de, obj) {
super.fromObject(de, obj);
// If updating this, also check the block in DeclarationReflection.fromObject.
this.packageName = obj.packageName;
this.packageVersion = obj.packageVersion;
if (obj.readme) {
this.readme = Comment.deserializeDisplayParts(de, obj.readme);
}
this.files.fromObject(de, obj.files || {});
de.defer(() => {
// Unnecessary conditional in release
for (const [id, sid] of Object.entries(obj.symbolIdMap || {})) {
const refl = this.getReflectionById(de.oldIdToNewId[+id] ?? -1);
if (refl) {
this.registerSymbolId(refl, new ReflectionSymbolId(sid));
}
else {
de.logger.warn(de.application.i18n.serialized_project_referenced_0_not_part_of_project(id.toString()));
}
}
});
}
}