ts-jsdoc
Version:
Transform TypeScript to JSDoc annotated JS code
522 lines • 21.3 kB
JavaScript
"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(` `);
}
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