builddocs
Version:
Generate HTML documentation from TypeScript types and doc comments
1,088 lines (1,087 loc) • 48.2 kB
JavaScript
import * as ts from "typescript";
import { resolve, dirname, relative, sep } from "node:path";
// Used for recursion check in getObjectType and getCallSignature
const gettingObjectTypes = [], gettingCallSignatures = [];
class Project {
tc;
basedir;
exports;
ids = new Set;
toResolve = [];
defs = new Map;
constructor(tc, exports, basedir) {
this.tc = tc;
this.exports = exports;
this.basedir = basedir;
}
makeID(id, isNamespace = false) {
if (this.ids.has(id)) {
if (isNamespace && !this.ids.has(id + "_ns")) {
id += "_ns";
}
else {
for (let i = 2;; i++)
if (!this.ids.has(id + "_" + i)) {
id += "_" + i;
break;
}
}
}
this.ids.add(id);
return id;
}
// Tells whether a symbol is either exported or external, and thus
// can be used in the output
isAvailable(symbol) {
return this.exports.has(symbol) || this.isExternal(symbol);
}
isExternal(symbol) {
let decl = maybeDecl(symbol);
if (!decl)
return true;
let path = resolve(decl.getSourceFile().fileName);
return !path.startsWith(this.basedir + sep) || /\bnode_modules\b/.test(path.slice(this.basedir.length));
}
define(sym, binding) {
let known = this.defs.get(sym);
if (!known || /^(typealias|class|interface)$/.test(binding))
this.defs.set(sym, binding);
}
nodePath(node) {
return relative(this.basedir, node.getSourceFile().fileName);
}
addQualifiedNames(items, prefix) {
for (let item of items) {
if (prefix)
item.qualifiedName = prefix + "." + item.name;
if (item.kind == "namespace" || item.kind == "enum")
this.addQualifiedNames(item.items, item.qualifiedName || item.name);
if (item.kind == "class" || item.kind == "interface" || item.kind == "typealias" && item.type == "object") {
for (let member of item.members) {
if (member.kind != "constructor" && (item.kind != "class" || member.static))
member.qualifiedName = (item.qualifiedName || item.name) + (member.name[0] == "[" ? "" : ".") + member.name;
}
}
}
}
resolve(modules) {
for (let mod of modules)
this.addQualifiedNames(mod.items);
for (let { symbol, ref } of this.toResolve) {
let found = this.defs.get(symbol);
if (found) {
ref.local = found.id;
if (found.qualifiedName)
ref.typeName = found.qualifiedName;
}
else if (!this.isExternal(symbol)) {
throw new Error(`Failed to resolve reference to ${ref.typeName}`);
}
}
}
sourcePos(node) {
if (!node)
return "";
let pos = ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.pos);
return ` at ${this.nodePath(node)}:${pos.line + 1}:${pos.character}`;
}
definedClassMembers(decl) {
let props = [], staticProps = [], ctors = [];
for (let member of decl.members) {
let symbol = this.tc.getSymbolAtLocation(member.name || member);
if (ts.isConstructorDeclaration(member)) {
ctors.push(member);
for (let param of member.parameters) {
if (ts.getCombinedModifierFlags(param) & (ts.ModifierFlags.Public | ts.ModifierFlags.Readonly))
props.push(this.tc.getSymbolAtLocation(param.name).name);
}
}
else if (ts.getCombinedModifierFlags(member) & ts.ModifierFlags.Static) {
staticProps.push(symbol.name);
}
else {
props.push(symbol.name);
}
}
return { props, staticProps, ctors };
}
}
class Context {
p;
id;
typeParams;
constructor(p, id, typeParams) {
this.p = p;
this.id = id;
this.typeParams = typeParams;
}
extend(name) {
return new Context(this.p, this.id ? this.id + "." + name : name, this.typeParams);
}
addParams(typeParams) {
return new Context(this.p, this.id, typeParams.concat(this.typeParams));
}
gatherItems(symbols, target = []) {
for (let symbol of symbols.filter(s => maybeDecl(s)).sort(compareSymbols)) {
let name = symbolName(symbol);
this.extend(name).itemsForSymbol(target, name, symbol);
}
}
itemForValueSymbol(name, symbol, kind) {
let decls = valueDecls(symbol);
let data = this.bindingData(name, decls), nonAbstract = false;
if (data.description && /\b/.test(data.description)) {
data.description = data.description.replace(/\s*\b/, "");
nonAbstract = true;
}
let mods = symbol.valueDeclaration ? ts.getCombinedModifierFlags(symbol.valueDeclaration) : 0;
if (isHidden(data.description))
return null;
let tsType = this.symbolType(symbol, decls[0]);
let cx = this, item;
if (kind == "class") {
let params = this.getTypeParams(decls[0]);
if (params)
cx = cx.addParams(params);
item = Object.assign(cx.getClassType(tsType), data, { kind: "class" });
if (params)
item.typeParams = params;
if ((mods & ts.ModifierFlags.Abstract) && !nonAbstract)
item.abstract = true;
if (nonAbstract)
for (let i of item.members)
delete i.abstract;
this.p.define(symbol, item);
}
else if (kind == "enum") {
item = Object.assign(cx.getEnumType(symbol), data, { kind: "enum" });
this.p.define(symbol, item);
}
else {
let type = cx.getType(tsType, symbol);
if (kind == "function") {
if (type.type != "function")
this.unexpectedType(type, symbol, "function item");
item = Object.assign(type, data, { kind: "function" });
}
else if (kind == "reexport") {
if (type.type != "reference")
this.unexpectedType(type, symbol, "export item");
item = Object.assign(type, data, { kind: "reexport" });
}
else if (kind == "variable") {
item = Object.assign(type, data, { kind: "variable" });
if (mods & ts.ModifierFlags.Const)
item.readonly = true;
}
else {
throw new Error(`Unexpected value kind: ${kind}${this.p.sourcePos(symbol.declarations[0])}`);
}
}
this.p.define(symbol, item);
return item;
}
itemForTypeSymbol(name, symbol, kind) {
let decls = typeDecls(symbol);
let data = this.bindingData(name, decls);
if (isHidden(data.description))
return null;
let cx = this;
let params = this.getTypeParams(decls[0]);
if (params)
cx = cx.addParams(params);
let type = cx.getType(this.p.tc.getDeclaredTypeOfSymbol(symbol), undefined, symbol);
let item;
if (kind == "interface") {
if (type.type != "interface")
this.unexpectedType(type, symbol, "interface item");
item = Object.assign(type, data, { kind });
}
else {
item = Object.assign(type, data, { kind });
}
if (params)
item.typeParams = params;
this.p.define(symbol, item);
return item;
}
itemForNamespace(name, symbol) {
let items = [];
let data = this.bindingData(name, namespaceDecls(symbol), true, this.p.makeID(this.id, true));
let item = Object.assign(data, { kind: "namespace", type: "namespace", items });
if (symbol.exports)
this.gatherItems([...symbol.exports.values()].filter(sym => {
return sym.declarations?.length && !(ts.getCombinedModifierFlags(sym.declarations[0]) & ts.ModifierFlags.Static);
}), items);
return item;
}
itemsForSymbol(target, name, symbol) {
if (symbol.flags & ts.SymbolFlags.Alias) {
let aliased = this.p.tc.getAliasedSymbol(symbol);
if (this.p.isExternal(aliased)) {
let data = this.bindingData(name, symbol.declarations);
let exp = Object.assign(this.getReferenceType(this.p.tc.getAliasedSymbol(symbol)), data, {
kind: "reexport",
});
target.push(exp);
}
else {
this.itemsForSymbol(target, symbolName(aliased), aliased);
}
return;
}
let valueKind, typeKind, hasNamespace = false;
if (symbol.flags & ts.SymbolFlags.Enum)
valueKind = "enum";
if (symbol.flags & ts.SymbolFlags.EnumMember)
valueKind = "enummember";
if (symbol.flags & ts.SymbolFlags.Class)
valueKind = "class";
if (symbol.flags & ts.SymbolFlags.Function)
valueKind = "function";
if (symbol.flags & ts.SymbolFlags.Interface)
typeKind = "interface";
if (symbol.flags & ts.SymbolFlags.TypeAlias)
typeKind = "typealias";
if (symbol.flags & ts.SymbolFlags.Variable)
valueKind = "variable";
if (symbol.flags & (ts.SymbolFlags.NamespaceModule | ts.SymbolFlags.ValueModule))
hasNamespace = true;
if (!valueKind && !typeKind && !hasNamespace)
throw new Error(`Can not determine a kind for symbol ${symbol.escapedName} with flags ${symbol.flags}${this.p.sourcePos(maybeDecl(symbol))}`);
if (valueKind) {
let val = this.itemForValueSymbol(name, symbol, valueKind);
if (val)
target.push(val);
}
if (typeKind) {
let val = this.itemForTypeSymbol(name, symbol, typeKind);
if (val)
target.push(val);
}
if (hasNamespace)
target.push(this.itemForNamespace(name, symbol));
}
gatherMembers(symbols, target = []) {
for (let sym of symbols.filter(s => maybeDecl(s)).sort(compareSymbols)) {
if (sym.flags & (ts.SymbolFlags.PropertyOrAccessor | ts.SymbolFlags.Signature | ts.SymbolFlags.Method)) {
let name = symbolName(sym), member = this.extend(name).memberForSymbol(name, sym);
if (member)
target.push(member);
}
}
}
memberForSymbol(name, symbol) {
let data = this.bindingData(name, symbol.declarations), nonAbstract = false;
if (data.description && /\b/.test(data.description)) {
data.description = data.description.replace(/\s*\b/, "");
nonAbstract = true;
}
let mods = symbol.valueDeclaration ? ts.getCombinedModifierFlags(symbol.valueDeclaration) : 0;
if ((mods & ts.ModifierFlags.Private) || isHidden(data.description))
return null;
let tsType = this.symbolType(symbol, symbol.declarations[0]);
let type = this.getType(tsType), member = null;
if ((symbol.flags & ts.SymbolFlags.Optional) && type.type == "union" &&
type.typeArgs.some(t => t.type == "simple" && t.typeName == "undefined")) {
let rest = type.typeArgs.filter(t => t.type != "simple" || t.typeName != "undefined");
if (rest.length == 1)
type = rest[0];
else
type.typeArgs = rest;
}
if (symbol.flags & (ts.SymbolFlags.PropertyOrAccessor | ts.SymbolFlags.Signature)) {
member = Object.assign(type, data, { kind: "property" });
if (mods & ts.ModifierFlags.Abstract)
member.abstract = true;
if (mods & ts.ModifierFlags.Static)
member.static = true;
if (symbol.flags & ts.SymbolFlags.Optional)
member.optional = true;
if ((mods & ts.ModifierFlags.Readonly) ||
((symbol.flags & (ts.SymbolFlags.GetAccessor | ts.SymbolFlags.SetAccessor)) == ts.SymbolFlags.GetAccessor))
member.readonly = true;
}
else if (symbol.flags & ts.SymbolFlags.Method) {
if (type.type != "function")
this.unexpectedType(type, symbol, "method member");
member = Object.assign(type, data, { kind: "method" });
if ((mods & ts.ModifierFlags.Abstract) && !nonAbstract)
member.abstract = true;
if (mods & ts.ModifierFlags.Static)
member.static = true;
}
if (member && (mods & ts.ModifierFlags.Protected))
member.protected = true;
if (member && (mods & ts.ModifierFlags.Static))
this.p.define(symbol, member);
return member;
}
getEnumType(symbol) {
let items = [];
for (let item of symbol.exports.values()) {
let name = symbolName(item);
let data = this.extend(name).bindingData(name, item.declarations);
if (isHidden(data.description))
continue;
let ref = {
type: "reference",
source: this.p.nodePath(symbol.valueDeclaration),
typeName: symbolName(symbol)
};
this.p.toResolve.push({ symbol, ref });
let member = Object.assign(ref, data, { kind: "enummember" });
this.p.define(item, member);
items.push(member);
}
return { type: "enum", items: items };
}
getType(type, valSymbol, typeSymbol) {
if (type.aliasSymbol && !(typeSymbol && (typeSymbol.flags & ts.SymbolFlags.TypeAlias)) &&
this.p.isAvailable(type.aliasSymbol))
return this.getReferenceType(type.aliasSymbol, type.aliasTypeArguments);
if (!(type.flags & (ts.TypeFlags.Object | ts.TypeFlags.Enum | ts.TypeFlags.EnumLiteral)) &&
type.symbol && type.symbol != valSymbol && type.symbol != typeSymbol &&
this.p.isAvailable(type.symbol))
return { type: "typeof", inner: this.getReferenceType(type.symbol) };
if (type.flags & ts.TypeFlags.Any)
return simple("any");
if (type.flags & ts.TypeFlags.String)
return simple("string");
if (type.flags & ts.TypeFlags.Number)
return simple("number");
if (type.flags & ts.TypeFlags.BigInt)
return simple("bigint");
if (type.flags & (ts.TypeFlags.ESSymbol | ts.TypeFlags.UniqueESSymbol))
return simple("symbol");
if (type.flags & ts.TypeFlags.Boolean)
return simple("boolean");
if (type.flags & ts.TypeFlags.Undefined)
return simple("undefined");
if (type.flags & ts.TypeFlags.Null)
return simple("null");
if (type.flags & ts.TypeFlags.Void)
return simple("void");
if (type.flags & ts.TypeFlags.TemplateLiteral)
return {
type: "template",
typeArgs: type.types.map(t => this.getType(t))
};
if ((type.flags & ts.TypeFlags.EnumLiteral) && this.p.isAvailable(type.symbol))
return this.getReferenceType(type.symbol);
// FIXME TypeScript doesn't export this. See https://github.com/microsoft/TypeScript/issues/26075, where they intend to fix that
if (type.flags & ts.TypeFlags.BooleanLiteral)
return { type: "literal", value: type.intrinsicName == "true" };
if (type.flags & (ts.TypeFlags.StringLiteral | ts.TypeFlags.NumberLiteral))
return { type: "literal", value: type.value };
if (type.flags & ts.TypeFlags.Never)
return simple("never");
if (type.flags & ts.TypeFlags.UnionOrIntersection) {
// TS keeps the denormalized type, but it's marked internal
type = (type.origin) || type;
let types = type.types;
let union = type.flags & ts.TypeFlags.Union;
let args = types.map(type => (type.flags & ts.TypeFlags.Void) ? simple("undefined") : this.getType(type));
// If both true and false occur in the union, combine them into boolean
if (union && args.some(a => a.type == "literal" && a.value === true) &&
args.some(a => a.type == "literal" && a.value === false))
args = [simple("boolean")].concat(args.filter(a => a.type != "literal" || typeof a.value != "boolean"));
// Move null and undefined types to the end
for (let tp of ["null", "undefined"]) {
let index = args.findIndex(a => a.type == "simple" && a.typeName == tp);
if (index > -1 && index != args.length - 1)
args.push(args.splice(index, 1)[0]);
}
return args.length == 1 ? args[0] : {
type: union ? "union" : "intersection",
typeArgs: args
};
}
if (type.flags & ts.TypeFlags.TypeParameter) {
let name = type.symbol.name, found = this.typeParams.find(p => p.name == name);
let decl = maybeDecl(type.symbol);
if (!found && decl?.parent?.kind != ts.SyntaxKind.InferType)
throw new Error(`Unknown type parameter ${name}${this.p.sourcePos(decl)}`);
let param = { type: "reference", typeName: name,
local: found ? found.id : this.id, source: decl && this.p.nodePath(decl) };
return found || !inConditionalExtends ? param : { type: "infer", inner: param };
}
if (type.flags & ts.TypeFlags.Index) {
return { type: "keyof", inner: this.getType(type.type) };
}
if (type.flags & ts.TypeFlags.IndexedAccess) {
return { type: "indexed", key: this.getType(type.indexType),
inner: this.getType(type.objectType) };
}
if (type.flags & ts.TypeFlags.Conditional) {
let { root } = type;
let prev = inConditionalExtends;
inConditionalExtends = true;
let extendsType = this.getType(root.extendsType);
inConditionalExtends = prev;
return { type: "conditional",
inner: this.getType(root.checkType),
extends: extendsType,
true: this.getType(this.p.tc.getTypeFromTypeNode(root.node.trueType)),
false: this.getType(this.p.tc.getTypeFromTypeNode(root.node.falseType)) };
}
if (type.flags & ts.TypeFlags.Object) {
let objFlags = type.objectFlags;
if (valSymbol && (valSymbol.flags & ts.SymbolFlags.Class))
return this.getClassType(type);
if (typeSymbol && (typeSymbol.flags & ts.SymbolFlags.Interface))
return this.getObjectType(type, typeSymbol);
if ((objFlags & (ts.ObjectFlags.Reference | ts.ObjectFlags.Interface)) &&
type.symbol && this.p.isAvailable(type.symbol))
return this.getReferenceType(type.symbol, type.typeArguments);
// Tuples have a weird structure where they point as references at a generic tuple type
if (objFlags & ts.ObjectFlags.Reference) {
let target = type.target;
if ((target.flags & ts.TypeFlags.Object) && (target.objectFlags & ts.ObjectFlags.Tuple))
return { type: "tuple", typeArgs: type.typeArguments.map(t => this.getType(t)) };
}
if (objFlags & ts.ObjectFlags.Mapped) {
// Instantiated types replace the type they are defined as with
// some messy out-of context instantiation of the way that type
// itself is defined. Here we try to fish the original
// definition out of the syntax tree.
if (objFlags & ts.ObjectFlags.Instantiated) {
let decl = typeSymbol && typeSymbol.declarations.find(d => ts.isTypeAliasDeclaration(d));
if (decl && ts.isTypeReferenceNode(decl.type)) {
let ref = decl.type;
let name = this.p.tc.getTypeAtLocation(ref.typeName);
let args = (ref.typeArguments || []).map(n => this.p.tc.getTypeAtLocation(n));
let sym = name.aliasSymbol || name.symbol;
if (sym)
return this.getReferenceType(sym, args);
}
return simple("any");
}
let mappedDecl = type.symbol?.declarations?.find(d => ts.isMappedTypeNode(d));
if (mappedDecl) {
let key = this.getTypeParam(mappedDecl.typeParameter);
let cx = this.addParams([key]);
let inner = mappedDecl.type ? cx.getType(this.p.tc.getTypeAtLocation(mappedDecl.type)) : simple("any");
return { type: "mapped", key, inner };
}
}
if (type.symbol && (type.symbol.flags & (ts.SymbolFlags.Class | ts.SymbolFlags.Enum | ts.SymbolFlags.ValueModule)) &&
this.p.isAvailable(type.symbol) && type.symbol != valSymbol && type.symbol != typeSymbol)
return { type: "typeof", inner: this.getReferenceType(type.symbol) };
return this.getObjectType(type, objFlags & ts.ObjectFlags.Interface ? type.symbol : undefined);
}
if (type.flags & ts.TypeFlags.Unknown)
return simple("unknown");
if (type.flags & ts.TypeFlags.NonPrimitive)
return { type: "object", members: [] };
let refDecl = (valSymbol || typeSymbol || type.symbol)?.declarations?.[0];
throw new Error(`Unsupported type ${this.p.tc.typeToString(type)} with flags ${type.flags}${this.p.sourcePos(refDecl)}`);
}
getObjectType(type, interfaceSymbol) {
if (gettingObjectTypes.includes(type))
return { type: "object", members: [] };
gettingObjectTypes.push(type);
try {
let members = [], signatures;
let implemented;
let callSigs = type.getCallSignatures();
if (callSigs.length)
signatures = callSigs.map(s => this.getCallSignature(s, "function"));
let ctorSigs = type.getConstructSignatures();
if (ctorSigs.length) {
let ctor = this.getConstructor(ctorSigs, true);
if (ctor)
members.push(ctor);
}
let props = type.getProperties();
let intDecl = interfaceSymbol && maybeDecl(interfaceSymbol);
let tsMembers;
if (intDecl && ts.isInterfaceDeclaration(intDecl)) {
let declared = intDecl.members.filter(member => member.name).map(member => this.p.tc.getSymbolAtLocation(member.name).name);
props = props.filter(prop => declared.includes(prop.name));
tsMembers = intDecl.members;
if (intDecl.heritageClauses && intDecl.heritageClauses.length)
implemented = intDecl.heritageClauses[0].types.map(node => this.getType(this.p.tc.getTypeAtLocation(node)));
}
this.gatherMembers(props, members);
let strIndex = type.getStringIndexType(), numIndex = type.getNumberIndexType();
if (strIndex || numIndex) {
if (!tsMembers) {
let sym = type.getSymbol(), decl = sym && maybeDecl(sym);
if (decl && ts.isTypeLiteralNode(decl))
tsMembers = decl.members;
}
let indexSym;
if (tsMembers)
for (let m of tsMembers)
if (ts.isIndexSignatureDeclaration(m))
indexSym = m.symbol;
if (indexSym) {
let indexMember = this.extend(strIndex ? "string" : "number")
.memberForSymbol(`[${strIndex ? "string" : "number"}]`, indexSym);
if (indexMember) {
if (indexMember.type == "simple" && indexMember.typeName == "any") {
delete indexMember["typeName"];
delete indexMember["type"];
indexMember = Object.assign(this.getType(strIndex || numIndex), indexMember);
}
members.push(indexMember);
}
}
}
if (signatures && !intDecl && !members.length)
return { type: "function", signatures };
let result = intDecl ? { type: "interface", members } : { type: "object", members };
if (signatures)
result.signatures = signatures;
if (intDecl && implemented)
result.implements = implemented;
return result;
}
finally {
gettingObjectTypes.pop();
}
}
getClassType(type) {
let classDecl = type.symbol?.valueDeclaration;
if (!classDecl || !ts.isClassDeclaration(classDecl))
throw new Error("Class decl isn't class-like");
let members = [];
let out = { type: "class", members };
let parentProps = [], hiddenParent, implemented;
if (classDecl.heritageClauses) {
for (let heritage of classDecl.heritageClauses) {
for (let node of heritage.types) {
let tsType = this.p.tc.getTypeAtLocation(node);
let sym = tsType.aliasSymbol || tsType.symbol;
if (!sym || this.p.isAvailable(sym)) {
for (let sym of tsType.getProperties())
parentProps.push(sym.name);
let type = this.getType(tsType);
if (heritage.token == ts.SyntaxKind.ExtendsKeyword)
out.extends = type;
else
(implemented || (out.implements = implemented = [])).push(type);
}
else if (heritage.token == ts.SyntaxKind.ExtendsKeyword) {
hiddenParent = (tsType.aliasSymbol || tsType.symbol).declarations?.find(d => ts.isClassDeclaration(d));
}
}
}
}
let defined = this.p.definedClassMembers(classDecl);
if (hiddenParent) {
let hidden = this.p.definedClassMembers(hiddenParent);
if (!defined.ctors.length)
defined.ctors = hidden.ctors;
for (let prop of hidden.props)
if (!defined.props.includes(prop))
defined.props.push(prop);
for (let prop of hidden.staticProps)
if (!defined.staticProps.includes(prop))
defined.staticProps.push(prop);
}
let ctorMember = this.getConstructor(type.getConstructSignatures()
.filter(sig => defined.ctors.includes(sig.declaration)), false);
// FIXME I haven't found a less weird way to get the instance type
let ctorType = type.getConstructSignatures()[0];
let props = type.getProperties().filter(prop => defined.staticProps.includes(prop.name))
.concat(ctorType.getReturnType().getProperties().filter(prop => defined.props.includes(prop.name)))
.sort(compareSymbols);
this.gatherMembers(props, members);
members = out.members = members.filter(item => !!item.description || !parentProps.includes(item.name));
if (ctorMember)
addSorted(members, ctorMember);
return out;
}
getReferenceType(symbol, typeArgs) {
let result = { type: "reference", typeName: symbol.name };
this.p.toResolve.push({ symbol, ref: result });
let decl = typeDecls(symbol)[0] || valueDecls(symbol)[0];
let source = this.p.nodePath(decl);
if (!isBuiltin(source))
result.source = source;
if (typeArgs) {
let decl = typeDecls(symbol)[0] || symbol.declarations?.[0];
let targetParams = decl?.typeParameters || [];
let args = typeArgs.slice(0, targetParams.length).map(arg => this.getType(arg));
// If there are default types for the type parameters, drop
// types that match the default from the list of arguments to
// reduce noise.
let cx, paramMap;
for (let i = targetParams.length - 1; i >= 0; i--) {
let deflt = targetParams[i].default;
if (!deflt)
break;
if (!cx) {
cx = this.addParams(args.map((a, i) => ({
kind: "typeparam",
name: targetParams[i].name.text,
id: this.id + "." + String(i)
})));
paramMap = Object.create(null);
for (let i = 0; i < args.length; i++)
paramMap[cx.typeParams[this.typeParams.length + i].id] = args[i];
}
let compare = cx.getType(this.p.tc.getTypeAtLocation(deflt));
if (compareTypes(args[i], compare, paramMap))
args.pop();
else
break;
}
if (args.length)
result.typeArgs = args;
}
return result;
}
getParams(signature) {
let result = [];
for (let param of signature.getParameters()) {
let name = ts.isIdentifier(param.valueDeclaration.name) ? param.name : "";
let cx = this.extend(name || "arg"), optional = false, type = cx.symbolType(param, valueDecls(param)[0]);
let decl = param.valueDeclaration;
if (decl && decl.questionToken) {
optional = true;
type = this.p.tc.getNonNullableType(type);
}
let isInitArg = decl && (ts.getCombinedModifierFlags(decl) & (ts.ModifierFlags.Public | ts.ModifierFlags.Readonly));
let data = cx.bindingData(name, decl ? [decl] : [], !isInitArg);
if (isHidden(data.description))
continue;
let p = Object.assign(cx.getType(type, param), data, {
kind: "param"
});
let deflt = decl && decl.initializer;
if (deflt)
p.defaultValue = deflt.getSourceFile().text.slice(deflt.pos, deflt.end).trim();
if (deflt || optional)
p.optional = true;
if (decl && decl.dotDotDotToken)
p.rest = true;
this.p.define(param, p);
result.push(p);
}
return result;
}
getTypeParams(decl) {
let params = decl.typeParameters;
return !params ? null : params.reduce(([res, cx], param) => {
let p = cx.getTypeParam(param);
return [res.concat(p), cx.addParams([p])];
}, [[], this])[0];
}
getTypeParam(param) {
let sym = this.p.tc.getSymbolAtLocation(param.name);
let localCx = this.extend(sym.name);
let data = localCx.bindingData(sym.name, [param]);
let result = Object.assign(data, { kind: "typeparam" });
let constraint = ts.getEffectiveConstraintOfTypeParameter(param);
// Directly querying getTypeAtLocation for the constraint will
// resolve keyof types for some reason, which can lead to very
// ugly and verbose output. So this inspects the type node for
// that case and manually handles it.
if (constraint && ts.isTypeOperatorNode(constraint) && constraint.operator == ts.SyntaxKind.KeyOfKeyword)
result.extends = { type: "keyof", inner: this.getType(this.p.tc.getTypeAtLocation(constraint.type)) };
else if (constraint)
result.extends = localCx.addParams([result]).getType(this.p.tc.getTypeAtLocation(constraint));
if (param.default)
result.default = localCx.getType(this.p.tc.getTypeAtLocation(param.default));
this.p.define(sym, result);
return result;
}
getConstructor(signatures, includeRet) {
let ctorSignatures = [], result, ctorID;
for (let signature of signatures) {
let decl = signature.declaration;
let flags = decl ? ts.getCombinedModifierFlags(decl) : 0;
if (!decl || (flags & ts.ModifierFlags.Private))
continue;
let data = this.bindingData("constructor", [decl], true, ctorID || (ctorID = this.p.makeID(this.id + ".constructor")));
if (isHidden(data.description))
continue;
ctorSignatures.push(this.extend("constructor").getCallSignature(signature, "constructor", includeRet));
if (!result || data.description) {
result = Object.assign(data, {
kind: "constructor",
type: "function",
signatures: ctorSignatures
});
if (flags & ts.ModifierFlags.Protected)
result.protected = true;
}
}
return result;
}
getCallSignature(signature, type, includeRet = true) {
if (gettingCallSignatures.includes(signature))
return { type, params: [], returns: simple("void") };
gettingCallSignatures.push(signature);
let cx = this;
let typeParams = signature.typeParameters && this.getTypeParams(signature.getDeclaration());
let out = { type };
if (typeParams) {
cx = cx.addParams(typeParams);
out.typeParams = typeParams;
}
out.params = cx.getParams(signature);
if (includeRet) {
let pred = this.p.tc.getTypePredicateOfSignature(signature);
if (pred && pred.type) {
out.returns = { type: "predicate", inner: cx.getType(pred.type), target: pred.parameterName || "this" };
}
else {
let ret = signature.getReturnType();
if (!(ret.flags & ts.TypeFlags.Void))
out.returns = cx.extend("returns").getType(ret);
}
}
gettingCallSignatures.pop();
return out;
}
symbolType(symbol, decl) {
let type = this.p.tc.getTypeOfSymbolAtLocation(symbol, decl);
// FIXME this is weird and silly but for interface declarations TS gives a symbol type of any
if (type.flags & ts.TypeFlags.Any)
type = this.p.tc.getDeclaredTypeOfSymbol(symbol);
return type;
}
bindingData(name, nodes, comments = true, id) {
let result = { name, id: id || this.p.makeID(this.id) };
if (comments) {
let comment = "";
for (let node of nodes) {
let c = getComments(node);
if (c)
comment += (comment ? "\n\n" : "") + c;
}
if (comment)
result.description = comment;
}
for (let node of nodes) {
let sourceFile = node.getSourceFile();
if (sourceFile) {
let { pos } = nodes[0];
while (ts.isWhiteSpaceLike(sourceFile.text.charCodeAt(pos)))
++pos;
const { line, character } = ts.getLineAndCharacterOfPosition(sourceFile, pos);
result.loc = { file: this.p.nodePath(nodes[0]), line: line + 1, column: character };
break;
}
}
return result;
}
unexpectedType(type, symbol, kind) {
throw new Error(`Unexpected type (${type.type}) for ${kind}${this.p.sourcePos(symbol.declarations[0])}`);
}
}
function simple(tag) { return { type: "simple", typeName: tag }; }
function filterDecls(nodes, filter) {
return nodes.every(filter) ? nodes : nodes.filter(filter);
}
function valueDecls(symbol) {
return filterDecls(symbol.declarations, n => !(ts.isTypeAliasDeclaration(n) ||
ts.isInterfaceDeclaration(n) || ts.isModuleDeclaration(n)));
}
function typeDecls(symbol) {
return filterDecls(symbol.declarations, n => ts.isTypeAliasDeclaration(n) || ts.isInterfaceDeclaration(n));
}
function namespaceDecls(symbol) {
return filterDecls(symbol.declarations, n => ts.isModuleDeclaration(n));
}
function symbolName(symbol) {
if (!/^__@/.test(symbol.name))
return symbol.name;
let name = symbol.name.slice(3).match(/^[^@]*/)[0];
return name == "sym" ? "[unique symbol]" : `[symbol ${name}]`;
}
function maybeDecl(symbol) {
return symbol && (symbol.valueDeclaration || (symbol.declarations && symbol.declarations[0]));
}
let inConditionalExtends = false;
function addSorted(bindings, binding) {
let i = 0;
while (i < bindings.length && lineFor(bindings[i]) < lineFor(binding))
i++;
bindings.splice(i, 0, binding);
}
function lineFor(binding) {
return binding.loc ? binding.loc.line : 0;
}
function isBuiltin(path) {
return /typescript\/lib\/.*\.es\d+.*\.d\.ts$/.test(path);
}
function compareSymbols(a, b) {
let da = maybeDecl(a), db = maybeDecl(b);
if (!da)
return db ? -1 : 0;
if (!db)
return 1;
let fa = da.getSourceFile().fileName, fb = db.getSourceFile().fileName;
return fa == fb ? da.pos - db.pos : fa < fb ? -1 : 1;
}
function compareTypes(a, b, paramMap) {
if (a == b)
return true;
if (!a || !b)
return false;
if (a.type == "reference" && a.local)
a = paramMap[a.local] || a;
if (b.type == "reference" && b.local)
b = paramMap[b.local] || b;
if (a.type != b.type)
return false;
let B = b; // Can't find a practical way to convince TS to narrow b here in the switch body
switch (a.type) {
case "reference":
return a.typeName == B.typeName && a.source == B.source && compareTypeArgs(a.typeArgs, B.typeArgs, paramMap);
case "namespace":
case "enum":
return compareItems(a.items, B.items, paramMap);
case "simple":
return a.typeName == B.typeName;
case "union":
case "intersection":
case "template":
case "tuple":
return compareTypeArgs(a.typeArgs, B.typeArgs, paramMap);
case "literal":
return a.value === B.value;
case "keyof":
case "infer":
case "typeof":
case "predicate":
return compareTypes(a.inner, B.inner, paramMap);
case "interface":
case "object":
return compareSignatures(a.signatures, B.signatures, paramMap) && compareItems(a.members, B.members, paramMap);
case "class":
return compareItems(a.members, B.members, paramMap);
case "function":
return compareSignatures(a.signatures, B.signatures, paramMap);
case "mapped":
return compareTypes(a.key.extends, B.key.implements, paramMap) && compareTypes(a.inner, B.inner, paramMap);
case "conditional":
return compareTypes(a.inner, B.inner, paramMap) && compareTypes(a.true, B.true, paramMap) &&
compareTypes(a.false, B.false, paramMap);
case "indexed":
return compareTypes(a.key, B.key, paramMap) && compareTypes(a.inner, B.inner, paramMap);
}
}
function compareItems(a, b, paramMap) {
if (a.length != b.length)
return false;
for (let i = 0; i < a.length; i++)
if (a[i].name != b[i].name || !compareTypes(a[i], b[i], paramMap))
return false;
return true;
}
function compareSignatures(a, b, paramMap) {
if (a == b)
return true;
if (!a || !b || a.length != b.length)
return false;
for (let i = 0; i < a.length; i++) {
let sA = a[i], sB = b[i];
if (sA.returns != sB.returns && (!sA.returns || !sB.returns || !compareTypes(sA.returns, sB.returns, paramMap)))
return false;
if (sA.params.length != sB.params.length ||
!sA.params.every((p, i) => compareTypes(p, sB.params[i], paramMap)))
return false;
}
return true;
}
function compareTypeArgs(a, b, paramMap) {
return a ? !!b && a.length == b.length && a.every((t, i) => compareTypes(t, b[i], paramMap)) : !b;
}
const commentCache = new WeakMap();
function getComments(node) {
if (ts.isVariableDeclaration(node))
node = node.parent.parent;
let cached = commentCache.get(node);
if (cached != null)
return cached;
let { pos } = node;
const sourceFile = node.getSourceFile();
if (!sourceFile)
return ""; // Synthetic node
const { text } = sourceFile;
let lines = [], blankLine = false;
function add(line) {
if (blankLine) {
blankLine = false;
if (lines.length && /\S/.test(lines[lines.length - 1]))
lines.push("");
}
lines.push(line);
}
while (pos < text.length) {
const ch = text.charCodeAt(pos);
if (ch === 47) { // slash
const nextCh = text.charCodeAt(pos + 1);
if (nextCh === 47) {
let doc = text.charCodeAt(pos + 2) == 47;
let start = pos += doc ? 3 : 2;
while (pos < text.length && !ts.isLineBreak(text.charCodeAt(pos)))
pos++;
if (doc)
add(text.slice(start, pos));
}
else if (nextCh === 42) { // asterisk
const doc = text.charCodeAt(pos + 2) == 42, start = pos + (doc ? 3 : 2);
for (pos = start; pos < text.length; ++pos)
if (text.charCodeAt(pos) === 42 /* asterisk */ && text.charCodeAt(pos + 1) === 47 /* slash */)
break;
if (doc)
add(text.slice(start, pos));
pos += 2;
}
}
else if (ts.isWhiteSpaceLike(ch)) {
pos++;
if (ch == 10 && text.charCodeAt(pos) == 10)
blankLine = true;
}
else {
break;
}
}
let comment = stripComment(lines);
commentCache.set(node, comment);
return comment;
}
function stripComment(lines) {
for (var head, i = 1; i < lines.length; i++) {
var line = lines[i], lineHead = line.match(/^[\s\*]*/)[0];
if (lineHead != line) {
if (head == null) {
head = lineHead;
}
else {
var same = 0;
while (same < head.length && head.charCodeAt(same) == lineHead.charCodeAt(same))
++same;
if (same < head.length)
head = head.slice(0, same);
}
}
}
if (head != null) {
var startIndent = /^\s*/.exec(lines[0])[0];
var trailing = /\s*$/.exec(head)[0];
var extra = trailing.length - startIndent.length;
if (extra > 0)
head = head.slice(0, head.length - extra);
}
outer: for (var i = 0; i < lines.length; i++) {
var line = lines[i].replace(/\s+$/, "");
if (i == 0 && head != null) {
for (var j = 0; j < head.length; j++) {
var found = line.indexOf(head.slice(j));
if (found == 0) {
lines[i] = line.slice(head.length - j);
continue outer;
}
}
}
if (head == null || i == 0)
lines[i] = line.replace(/^[\s\*]*/, "");
else if (line.length < head.length)
lines[i] = "";
else
lines[i] = line.slice(head.length);
}
while (lines.length && !lines[lines.length - 1])
lines.pop();
while (lines.length && !lines[0])
lines.shift();
return lines.join("\n");
}
function isHidden(description) {
return description && /@(internal|hidden)\b/.test(description);
}
function collectExports(exports, tc) {
let result = new Set();
let explore = (sym) => {
if (result.has(sym) || sym.declarations?.some(decl => isHidden(getComments(decl))))
return;
result.add(sym);
let alias = (sym.flags & ts.SymbolFlags.Alias) ? tc.getAliasedSymbol(sym) : null;
if (alias)
explore(alias);
if ((sym.flags & (ts.SymbolFlags.NamespaceModule | ts.SymbolFlags.ValueModule | ts.SymbolFlags.Enum)) && sym.exports)
for (let child of sym.exports.values())
explore(child);
};
exports.forEach(explore);
return result;
}
function gatherTemplate(file, exports) {
let parts = [];
for (let m, re = /(^|\n)\s*\/\/- ?(.*)/g; m = re.exec(file.text);)
parts.push({ from: m.index + m[1].length, to: m.index + m[0].length, text: m[2] });
for (let e of exports) {
let local = e.declarations ? e.declarations.filter(d => d.getSourceFile() == file) : [];
let use = (local.length > 1 && local.find(e => ts.isExportSpecifier(e))) || (local.length ? local[0] : null);
if (use)
parts.push({ from: use.pos, export: symbolName(e) });
}
parts.sort((a, b) => a.from - b.from);
let result = [];
let current = "", lastPos = -1;
for (let part of parts) {
if ("text" in part) {
if (current)
current += "\n" + (lastPos < part.from ? "\n" : "") + part.text;
else
current = part.text;
lastPos = part.to + 1;
}
else {
if (current) {
result.push(current);
current = "";
lastPos = -1;
}
result.push({ export: part.export });
}
}
if (current)
result.push(current);
return result;
}
/// Gather the types and documentation for the given modules.
export function getDocs(spec) {
if (!spec.modules.length)
return [];
let filenames = spec.modules.map(s => s.filename);
let configPath = ts.findConfigFile(filenames[0], ts.sys.fileExists);
let options = { noCheck: true }, host = ts.createCompilerHost(options);
if (configPath)
options = ts.getParsedCommandLineOfConfigFile(configPath, options, host).options;
let program = ts.createProgram({ rootNames: filenames, options, host });
let tc = program.getTypeChecker();
let basedir = resolve(spec.basedir || dirname(configPath || filenames[0]));
let exportsByModule = spec.modules.map(({ filename }) => {
let sourceFile = program.getSourceFile(filename);
if (!sourceFile)
throw new Error(`Source file "${filename}" not found`);
let fileSymbol = tc.getSymbolAtLocation(sourceFile);
if (!fileSymbol)
throw new Error(`No symbol for file "${filename}" (no exports?)`);
return tc.getExportsOfModule(fileSymbol);
});
let exports = collectExports(exportsByModule.reduce((a, b) => a.concat(b)), tc);
let project = new Project(tc, exports, basedir);
let mods = spec.modules.map(({ filename, modname }, i) => {
if (spec.separate)
project = new Project(tc, exports, basedir);
let items = [];
new Context(project, modname || "", []).gatherItems(exportsByModule[i], items);
let mod = { items, modname, filename, template: gatherTemplate(program.getSourceFile(filename), exportsByModule[i]) };
if (spec.separate)
project.resolve([mod]);
return mod;
});
project.resolve(mods);
return mods;
}