ts-jsdoc
Version:
Transform TypeScript to JSDoc annotated JS code
277 lines • 11.6 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.JsDocRenderer = void 0;
const ts = require("typescript");
const path = require("path");
const doctrine_1 = require("doctrine");
class JsDocRenderer {
constructor(generator) {
this.generator = generator;
this.indent = "";
}
normalizeDescription(comment) {
return this.indent + " * " + comment
.split("\n")
.map(it => it.trim())
.filter(it => it != "*/" && it.length > 0)
.join(`\n${this.indent} * `);
}
normalizeDescription2(comment) {
return this.indent + " * " + comment
.split("\n")
.map(it => it.trim())
.join(`\n${this.indent} * `);
}
formatComment(node, tags, description) {
const indent = this.indent;
let result = `${indent}/**\n`;
if (description == null) {
const comment = JsDocRenderer.getComment(node);
if (comment != null) {
result += `${this.normalizeDescription(comment)}\n`;
}
}
else if (description.length > 0) {
result += `${this.normalizeDescription2(description)}\n`;
}
// must be added after user description
if (tags.length > 0) {
for (const tag of tags) {
result += `${indent} * ${tag}\n`;
}
}
result += `${indent} */\n`;
return result;
}
renderClassOrInterface(descriptor, modulePathMapper, examples) {
this.indent = "";
const tags = [];
if (descriptor.isInterface) {
tags.push(`@interface ${descriptor.modulePath}.${descriptor.name}`);
}
for (const parent of descriptor.parents) {
// ignore <> type params because JsDoc expects namepath, but not type expression
tags.push(`@extends ${renderType(parent, modulePathMapper, true)}`);
}
JsDocRenderer.renderProperties(descriptor.properties, tags, modulePathMapper);
if (examples != null) {
for (const example of examples) {
tags.push(`@example <caption>${example.name}</caption> @lang ${example.lang}\n * ${example.content.trim().split("\n").join("\n * ")}`);
}
}
let result = this.formatComment(descriptor.node, tags, parseExistingJsDoc(descriptor.node, tags) || "");
result += `export class ${descriptor.name} {\n`;
this.indent = " ";
for (const method of descriptor.methods) {
result += this.renderMethod(method, modulePathMapper, descriptor);
if (method !== descriptor.methods[descriptor.methods.length - 1]) {
result += "\n";
}
}
this.indent = "";
result += "}\n\n";
return result;
}
renderMethod(method, modulePathMapper, classDescriptor) {
const tags = method.tags.slice();
const paramNameToInfo = new Map();
let returns = null;
const parsed = method.jsDoc;
if (parsed != null) {
for (const tag of parsed.tags) {
if (tag.title === "param") {
if (tag.name != null) {
paramNameToInfo.set(tag.name, tag);
}
}
else if (tag.title === "returns" || tag.title === "return") {
returns = tag;
}
else {
tags.push(printTag(tag));
}
}
}
for (const param of method.node.parameters) {
let name = param.name.text;
let text = `@param`;
const type = param.type;
if (type != null) {
let namePathByNode = this.generator.getTypeNamePathByNode(type);
if (namePathByNode == null) {
console.warn("cannot get namePathByNode for " + type);
}
else {
text += ` ${renderTypes(namePathByNode, modulePathMapper)}`;
}
}
text += ` ${name}`;
const tag = paramNameToInfo.get(name);
if (tag != null && tag.description != null) {
text += ` ${tag.description}`;
}
tags.push(text);
}
if (classDescriptor != null) {
// https://github.com/jsdoc3/jsdoc/issues/1137#issuecomment-281257286
tags.push(`@function ${classDescriptor.modulePath}.${classDescriptor.name}#${method.name}`);
}
const signature = this.generator.program.getTypeChecker().getSignatureFromDeclaration(method.node);
const returnTypes = this.generator.getTypeNames(signature.getReturnType(), method.node);
// http://stackoverflow.com/questions/4759175/how-to-return-void-in-jsdoc
if (!returnTypes.includes("void")) {
let text = `@returns ${renderTypes(returnTypes, modulePathMapper)}`;
if (returns != null) {
text += ` ${returns.description}`;
}
tags.push(text);
}
let result = this.formatComment(method.node, tags, (parsed == null ? "" : parsed.description) || "");
result += `${this.indent}`;
if (method.node.kind === ts.SyntaxKind.FunctionDeclaration) {
result += "export function ";
}
result += `${method.name}() {}\n`;
return result;
}
static getComment(node) {
const sourceFile = node.getSourceFile();
const leadingCommentRanges = ts.getLeadingCommentRanges(sourceFile.text, node.pos);
if (leadingCommentRanges == null || leadingCommentRanges.length === 0) {
return null;
}
const commentRange = leadingCommentRanges[0];
if (sourceFile.text[commentRange.pos] === "/" && sourceFile.text[commentRange.pos + 1] === "*" && sourceFile.text[commentRange.pos + 2] == "*") {
return sourceFile.text.slice(commentRange.pos + 3, commentRange.end).trim();
}
return null;
}
renderVariable(descriptor, modulePathMapper) {
this.indent = "";
const tags = [`@type ${renderTypes(descriptor.types, modulePathMapper)}`];
if (descriptor.isConst) {
tags.push("@constant");
}
let result = this.formatComment(descriptor.node, tags);
// jsdoc cannot parse const, so, we always use var
result += `export var ${descriptor.name}\n`;
return result;
}
renderMember(descriptor) {
const tags = [
"@enum {number}"
];
if (descriptor.readonly) {
tags.push("@readonly");
}
if (descriptor.properties != null) {
for (const property of descriptor.properties) {
tags.push(`@property ${property.name}`);
}
}
let result = this.formatComment(descriptor.node, tags);
result += `export var ${descriptor.name}\n`;
return result;
}
// form http://stackoverflow.com/questions/10490713/how-to-document-the-properties-of-the-object-in-the-jsdoc-3-tag-this
// doesn't produce properties table, so, we use property tags
static renderProperties(properties, tags, modulePathMapper) {
loop: for (const descriptor of properties) {
const node = descriptor.node;
const existingJsDoc = JsDocRenderer.getComment(node);
const parsed = existingJsDoc == null ? null : doctrine_1.parse(existingJsDoc, { unwrap: true });
let defaultValue = descriptor.defaultValue;
let isOptional = descriptor.isOptional;
let description = parsed == null ? "" : parsed.description;
if (parsed != null) {
for (const tag of parsed.tags) {
switch (tag.title) {
case "default":
defaultValue = tag.description;
break;
case "private":
continue loop;
case "required":
isOptional = false;
break;
case "see":
description += `\nSee: ${tag.description}`;
break;
case "deprecated":
description += `\nDeprecated: {tag.description}`;
break;
default: {
const sourceFile = node.getSourceFile();
const leadingCommentRanges = ts.getLeadingCommentRanges(sourceFile.text, node.pos);
const position = sourceFile.getLineAndCharacterOfPosition(leadingCommentRanges[0].pos);
console.warn(`${path.basename(sourceFile.fileName)} ${position.line + 1}:${position.character} property level tag "${tag.title}" are not supported, please file issue`);
}
}
}
}
let result = `@property ${renderTypes(descriptor.types, modulePathMapper)} `;
if (isOptional) {
result += "[";
}
result += descriptor.name;
if (defaultValue != null) {
result += `=${defaultValue}`;
}
if (isOptional) {
result += "]";
}
if (description != null) {
description = description.trim();
if (description.length > 0) {
// one \n is not translated to break as markdown does (because in the code newline means that we don't want to use long line and have to break)
description = description
.replace(/\n\n/g, "<br><br>")
.replace(/\n/g, " ");
// http://stackoverflow.com/questions/28733282/jsdoc-multiline-description-property
result += ` ${description}`;
}
}
tags.push(result);
}
}
}
exports.JsDocRenderer = JsDocRenderer;
function parseExistingJsDoc(node, tags) {
const existingJsDoc = JsDocRenderer.getComment(node);
const parsed = existingJsDoc == null ? null : doctrine_1.parse(existingJsDoc, { unwrap: true });
if (parsed != null) {
for (const tag of parsed.tags) {
tags.push(printTag(tag));
}
}
return parsed == null ? null : parsed.description;
}
function printTag(tag) {
let text = `@${tag.title}`;
const caption = tag.caption;
if (caption != null) {
text += ` <caption>${caption}</caption>`;
}
if (tag.description != null) {
text += ` ${tag.description}`;
}
return text;
}
// (oldPath: string) => oldPath
function renderTypes(names, modulePathMapper) {
return `{${_renderTypes(names, modulePathMapper)}}`;
}
function _renderTypes(names, modulePathMapper) {
return names.map(it => renderType(it, modulePathMapper)).join(" | ");
}
function renderType(name, modulePathMapper, ignoreSubtypes = false) {
if (typeof name === "string") {
return modulePathMapper(name);
}
const type = name;
if (ignoreSubtypes) {
return modulePathMapper(type.name);
}
return modulePathMapper(type.name) + "<" + _renderTypes(type.subTypes, modulePathMapper) + ">";
}
//# sourceMappingURL=JsDocRenderer.js.map
;