UNPKG

builddocs

Version:

Generate HTML documentation from TypeScript types and doc comments

1,088 lines (1,087 loc) 48.2 kB
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 && /@nonabstract\b/.test(data.description)) { data.description = data.description.replace(/\s*@nonabstract\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 && /@nonabstract\b/.test(data.description)) { data.description = data.description.replace(/\s*@nonabstract\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; }