UNPKG

ts-jsdoc

Version:

Transform TypeScript to JSDoc annotated JS code

522 lines 21.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.JsDocGenerator = exports.generate = void 0; const ts = require("typescript"); const path = require("path"); const JsDocRenderer_1 = require("./JsDocRenderer"); const util_1 = require("./util"); const doctrine_1 = require("doctrine"); const vm = require("vm"); function generate(basePath, config, moduleName, main, options) { const compilerOptions = config.options; const compilerHost = ts.createCompilerHost(compilerOptions); const program = ts.createProgram(config.fileNames, compilerOptions, compilerHost); util_1.checkErrors(ts.getPreEmitDiagnostics(program)); const compilerOutDir = compilerOptions.outDir; if (compilerOutDir == null) { throw new Error("outDir is not specified in the compilerOptions"); } const generator = new JsDocGenerator(program, path.relative(basePath, compilerOutDir), moduleName, main, program.getCommonSourceDirectory(), options, path.resolve(compilerOptions.baseUrl)); for (const sourceFile of program.getSourceFiles()) { if (!sourceFile.isDeclarationFile) { generator.generate(sourceFile); } } return generator; } exports.generate = generate; class JsDocGenerator { constructor(program, relativeOutDir, moduleName, mainFile, commonSourceDirectory, options, baseUrl) { this.program = program; this.relativeOutDir = relativeOutDir; this.moduleName = moduleName; this.mainFile = mainFile; this.commonSourceDirectory = commonSourceDirectory; this.options = options; this.baseUrl = baseUrl; this.moduleNameToResult = new Map(); this.currentSourceModuleId = ""; this.renderer = new JsDocRenderer_1.JsDocRenderer(this); this.mainMappings = new Map(); } sourceFileToModuleId(sourceFile) { if (sourceFile.isDeclarationFile) { if (sourceFile.fileName.endsWith("node.d.ts")) { return { id: "node", isMain: false }; } let fileNameWithoutExt = sourceFile.fileName.slice(0, sourceFile.fileName.length - ".d.ts".length).replace(/\\/g, "/"); if (this.baseUrl != null && fileNameWithoutExt.startsWith(this.baseUrl)) { fileNameWithoutExt = fileNameWithoutExt.substring(this.baseUrl.length + 1); } return { id: fileNameWithoutExt, isMain: false }; } let sourceModuleId; const fileNameWithoutExt = sourceFile.fileName.slice(0, sourceFile.fileName.lastIndexOf(".")).replace(/\\/g, "/"); const name = path.relative(this.commonSourceDirectory, fileNameWithoutExt); if (this.moduleName != null) { sourceModuleId = this.moduleName; if (name !== "index") { sourceModuleId += "/" + this.relativeOutDir; } } else { sourceModuleId = this.relativeOutDir; } if (name !== "index") { sourceModuleId += "/" + name; } const isMain = this.mainFile == null ? fileNameWithoutExt.endsWith("/main") : `${fileNameWithoutExt}.js`.includes(path.posix.relative(this.relativeOutDir, this.mainFile)); if (isMain) { sourceModuleId = this.moduleName; } return { id: sourceModuleId, isMain }; } generate(sourceFile) { if (sourceFile.text.length === 0) { return; } const moduleId = this.sourceFileToModuleId(sourceFile); this.currentSourceModuleId = moduleId.id; const classes = []; const functions = []; const members = []; util_1.processTree(sourceFile, (node) => { if (node.kind === ts.SyntaxKind.InterfaceDeclaration || node.kind === ts.SyntaxKind.ClassDeclaration) { const descriptor = this.processClassOrInterface(node); if (descriptor != null) { classes.push(descriptor); } } else if (node.kind === ts.SyntaxKind.FunctionDeclaration) { const descriptor = this.describeFunction(node); if (descriptor != null) { functions.push(descriptor); } } else if (moduleId.isMain && node.kind === ts.SyntaxKind.ExportDeclaration) { this.handleExportFromMain(node); return true; } else if (node.kind === ts.SyntaxKind.SourceFile) { return false; } else if (node.kind === ts.SyntaxKind.VariableStatement) { const descriptor = this.describeVariable(node); if (descriptor != null) { members.push(descriptor); } } else if (node.kind === ts.SyntaxKind.EnumDeclaration) { const descriptor = this.describeEnum(node); if (descriptor != null) { members.push(descriptor); } } return true; }); const existingPsi = this.moduleNameToResult.get(moduleId.id); if (existingPsi == null) { this.moduleNameToResult.set(moduleId.id, { classes, functions, members }); } else { existingPsi.classes.push(...classes); existingPsi.functions.push(...functions); existingPsi.members.push(...members); } } handleExportFromMain(node) { const moduleSpecifier = node.moduleSpecifier; const exportClause = node.exportClause; if (exportClause == null || moduleSpecifier == null) { return; } if (moduleSpecifier.kind !== ts.SyntaxKind.StringLiteral) { return; } const filePath = moduleSpecifier.text; if (!filePath.startsWith(".")) { return; } const fullFilename = path.posix.resolve(path.posix.dirname(node.getSourceFile().fileName), filePath) + ".ts"; const sourceFile = this.program.getSourceFile(fullFilename); if (sourceFile == null) { return; } const names = []; for (const e of exportClause.elements) { if (e.kind === ts.SyntaxKind.ExportSpecifier) { names.push(e.name.text); } else { console.error(`Unsupported export element: ${e.getText(e.getSourceFile())}`); } } this.mainMappings.set(this.sourceFileToModuleId(sourceFile).id, names); } getTypeNamePathByNode(node) { if (node.kind === ts.SyntaxKind.UnionType) { return this.typesToList(node.types, node); } else if (node.kind === ts.SyntaxKind.FunctionType) { return ["callback"]; } else if (node.kind === ts.SyntaxKind.NumberKeyword) { return ["number"]; } else if (node.kind === ts.SyntaxKind.StringKeyword) { return ["string"]; } else if (node.kind === ts.SyntaxKind.BooleanKeyword) { return ["boolean"]; } else if (node.kind === ts.SyntaxKind.NullKeyword) { return ["null"]; } else if (node.kind === ts.SyntaxKind.UndefinedKeyword) { return ["undefined"]; } else if (node.kind === ts.SyntaxKind.LiteralType) { const text = node.literal.text; return [`"${text}"`]; } else if (node.kind === ts.SyntaxKind.TypeLiteral) { // todo return ['Object.<string, any>']; } const type = this.program.getTypeChecker().getTypeAtLocation(node); return type == null ? null : this.getTypeNames(type, node); } typesToList(types, node) { const typeNames = []; for (const type of types) { if (type.kind == null) { const name = this.getTypeNamePath(type); if (name == null) { throw new Error(`Cannot get name for ${node.getText(node.getSourceFile())}`); } typeNames.push(name); } else { const name = this.getTypeNamePathByNode(type); if (name == null) { throw new Error(`Cannot get name for ${node.getText(node.getSourceFile())}`); } typeNames.push(...name); } } return typeNames; } getTypeNames(type, node) { if (type.flags & ts.TypeFlags.UnionOrIntersection && !(type.flags & ts.TypeFlags.Enum) && !(type.flags & ts.TypeFlags.EnumLiteral) && !(type.flags & ts.TypeFlags.Boolean) && !(type.flags & ts.TypeFlags.BooleanLiteral)) { return this.typesToList(type.types, node); } let result = this.getTypeNamePath(type); if (result == null) { return null; } const typeArguments = type.typeArguments; if (typeArguments != null) { const subTypes = []; for (const type of typeArguments) { const typeNames = this.getTypeNames(type, node); if (typeNames != null) { subTypes.push(...typeNames); } } return [{ name: result, subTypes: subTypes }]; } return [result]; } getTypeNamePath(type) { if (type.flags & ts.TypeFlags.Boolean) { return "boolean"; } if (type.flags & ts.TypeFlags.Void) { return "void"; } if (type.flags & ts.TypeFlags.Null) { return "null"; } if (type.flags & ts.TypeFlags.String) { return "string"; } if (type.flags & ts.TypeFlags.Number) { return "number"; } if (type.flags & ts.TypeFlags.Undefined) { return "undefined"; } if (type.flags & ts.TypeFlags.Any) { return "any"; } if (type.flags & ts.TypeFlags.Literal) { return `"${type.value}"`; } const symbol = type.symbol; if (symbol == null || symbol.declarations == null || symbol.declarations.length === 0) { return null; } const valueDeclaration = (symbol.valueDeclaration || ((symbol.declarations == null || symbol.declarations.length === 0) ? null : symbol.declarations[0])); if (ts.getCombinedModifierFlags(valueDeclaration) & ts.ModifierFlags.Ambient) { // Error from lib.es5.d.ts return symbol.name; } let typeSourceParent = valueDeclaration; while (typeSourceParent != null) { if (typeSourceParent.kind === ts.SyntaxKind.ModuleDeclaration && (typeSourceParent.flags & ts.NodeFlags.NestedNamespace) <= 0) { const m = typeSourceParent; const sourceModuleId = m.name.text; if (typeSourceParent.flags & ts.NodeFlags.Namespace) { return `${sourceModuleId}:${symbol.name}`; } else { return `module:${sourceModuleId}.${symbol.name}`; } } else if (typeSourceParent.kind === ts.SyntaxKind.SourceFile) { const sourceModuleId = this.sourceFileToModuleId(typeSourceParent).id; return `module:${sourceModuleId}.${symbol.name}`; } typeSourceParent = typeSourceParent.parent; } console.warn(`Cannot find parent for ${symbol}`); return null; } describeEnum(node) { const flags = ts.getCombinedModifierFlags(node); if (!(flags & ts.ModifierFlags.Export)) { return null; } const type = { names: ["number"] }; const name = node.name.text; const moduleId = this.computeTypePath(); const id = `${moduleId}.${name}`; const properties = []; for (const member of node.members) { const name = member.name.text; properties.push({ name: name, kind: "member", scope: "static", memberof: id, type: type, }); } // we don't set readonly because it is clear that enum is not mutable // e.g. jsdoc2md wil add useless "Read only: true" // noinspection SpellCheckingInspection return { node: node, id: id, name: name, longname: id, kind: "enum", scope: "static", memberof: moduleId, type: type, properties: properties, }; } describeVariable(node) { const declarations = node.declarationList == null ? null : node.declarationList.declarations; if (declarations == null || declarations.length !== 1) { return null; } const flags = ts.getCombinedModifierFlags(declarations[0]); if (!(flags & ts.ModifierFlags.Export)) { return null; } const declaration = declarations[0]; if (declaration.type == null) { return null; } const existingJsDoc = JsDocRenderer_1.JsDocRenderer.getComment(node); const jsDoc = existingJsDoc == null ? null : doctrine_1.parse(existingJsDoc, { unwrap: true }); if (JsDocGenerator.isHidden(jsDoc)) { return null; } let types; const type = this.program.getTypeChecker().getTypeAtLocation(declaration); if (type.symbol != null && type.symbol.valueDeclaration != null) { types = [this.getTypeNamePath(type)]; } else { types = this.getTypeNamePathByNode(declaration.type); } // NodeFlags.Const on VariableDeclarationList, not on VariableDeclaration return { types, node, name: declaration.name.text, isConst: (node.declarationList.flags & ts.NodeFlags.Const) > 0 }; } //noinspection JSMethodCanBeStatic describeFunction(node) { const flags = ts.getCombinedModifierFlags(node); if (!(flags & ts.ModifierFlags.Export)) { return null; } const existingJsDoc = JsDocRenderer_1.JsDocRenderer.getComment(node); const jsDoc = existingJsDoc == null ? null : doctrine_1.parse(existingJsDoc, { unwrap: true }); return JsDocGenerator.isHidden(jsDoc) ? null : { name: node.name.text, node: node, tags: [], jsDoc }; } static isHidden(jsDoc) { if (jsDoc == null) { return false; } for (const tag of jsDoc.tags) { if (tag.title === "internal" || tag.title === "private") { return true; } } return false; } processClassOrInterface(node) { const flags = ts.getCombinedModifierFlags(node); if (!(flags & ts.ModifierFlags.Export)) { return null; } const nodeDeclaration = node; const existingJsDoc = JsDocRenderer_1.JsDocRenderer.getComment(node); const jsDoc = existingJsDoc == null ? null : doctrine_1.parse(existingJsDoc, { unwrap: true }); if (JsDocGenerator.isHidden(jsDoc)) { return null; } const className = nodeDeclaration.name.text; const clazz = node; let parents = []; if (clazz.heritageClauses != null) { for (const heritageClause of clazz.heritageClauses) { if (heritageClause.types != null) { for (const type of heritageClause.types) { const typeNamePath = this.getTypeNamePathByNode(type); if (typeNamePath != null) { parents = parents.concat(typeNamePath); } } } } } const methods = []; const properties = []; for (const member of nodeDeclaration.members) { if (member.kind === ts.SyntaxKind.PropertySignature) { const p = this.describeProperty(member, node.kind === ts.SyntaxKind.ClassDeclaration); if (p != null) { properties.push(p); } } else if (member.kind === ts.SyntaxKind.PropertyDeclaration) { const p = this.describeProperty(member, node.kind === ts.SyntaxKind.ClassDeclaration); if (p != null) { properties.push(p); } } else if (member.kind === ts.SyntaxKind.GetAccessor) { const p = this.describeProperty(member, node.kind === ts.SyntaxKind.ClassDeclaration); if (p != null) { properties.push(p); } } else if (member.kind === ts.SyntaxKind.MethodDeclaration || member.kind === ts.SyntaxKind.MethodSignature) { const m = this.renderMethod(member); if (m != null) { methods.push(m); } } } methods.sort((a, b) => { let weightA = a.isProtected ? 100 : 0; let weightB = b.isProtected ? 100 : 0; // do not reorder getFeedURL/setFeedURL weightA += trimMutatorPrefix(a.name).localeCompare(trimMutatorPrefix(b.name)); return weightA - weightB; }); return { modulePath: this.computeTypePath(), name: className, node, methods, properties, parents, isInterface: node.kind === ts.SyntaxKind.InterfaceDeclaration }; } describeProperty(node, isParentClass) { const flags = ts.getCombinedModifierFlags(node); if (flags & ts.ModifierFlags.Private) { return null; } if (this.options.access === "public" && flags & ts.ModifierFlags.Protected) { return null; } const name = node.name.text; let types; if (node.type == null) { const type = this.program.getTypeChecker().getTypeAtLocation(node); if (type == null) { return null; } types = this.getTypeNames(type, node); } else { types = this.getTypeNamePathByNode(node.type); } let isOptional = node.questionToken != null; let defaultValue = null; const initializer = node.initializer; if (initializer != null) { const initializerText = initializer.getText(); if (initializer.expression != null || initializer.kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral || initializerText.includes("process.stdout")) { defaultValue = initializerText; } else { try { const sandbox = { sandboxVar: null }; vm.runInNewContext(`sandboxVar=${initializerText}`, sandbox); const val = sandbox.sandboxVar; if (val === null || typeof val === "string" || typeof val === "number" || "boolean" || Object.prototype.toString.call(val) === "[object Array]") { defaultValue = val; } else if (val) { console.warn(`unknown initializer for property ${name}: ${val}`); } } catch (e) { console.info(`exception evaluating "${initializerText}" for property ${name}`); defaultValue = initializerText; } } } isOptional = isOptional || defaultValue != null || types.includes("null"); if (!isOptional && isParentClass && (flags & ts.ModifierFlags.Readonly) > 0) { isOptional = true; } return { name, types, node, isOptional, defaultValue }; } renderMethod(node) { // node.flags doesn't report correctly for private methods const flags = ts.getCombinedModifierFlags(node); if (flags & ts.ModifierFlags.Private) { return null; } if (this.options.access === "public" && flags & ts.ModifierFlags.Protected) { return null; } const tags = []; const isProtected = (flags & ts.ModifierFlags.Protected) > 0; if (isProtected) { tags.push(`@protected`); } const name = node.name.text; const existingJsDoc = JsDocRenderer_1.JsDocRenderer.getComment(node); const jsDoc = existingJsDoc == null ? null : doctrine_1.parse(existingJsDoc, { unwrap: true }); return JsDocGenerator.isHidden(jsDoc) ? null : { name, tags, isProtected, node, jsDoc }; } computeTypePath() { return `module:${this.currentSourceModuleId}`; } } exports.JsDocGenerator = JsDocGenerator; function trimMutatorPrefix(name) { if (name.length > 4 && name[3] === name[3].toUpperCase() && (name.startsWith("get") || name.startsWith("set"))) { return name[3].toLowerCase() + name.substring(4); } return name; } //# sourceMappingURL=JsDocGenerator.js.map