// tslint:disable:no-bitwise
import * as ts from 'typescript';
function getTags(jsDoc: any) {
if (!jsDoc) {
return [];
}
const tags = jsDoc.tags as ts.JSDocTag[];
if (!tags) {
return [];
}
return tags.map((tag) => {
if (ts.isJSDocParameterTag(tag)) {
const type = 'param';
const name = (tag.name as ts.Identifier).text;
const description = tag.comment;
const string = `${name} ${tag.comment}`;
return { type, string, name, description, types: [], optional: false };
} else if (ts.isJSDocReturnTag(tag)) {
const type = 'return';
const description = tag.comment;
const string = tag.comment;
return { type, string, description, types: [], optional: false };
} else {
const type = tag.tagName.text.toLowerCase();
if (type === 'memberof') {
const parent = tag.comment;
const string = tag.comment;
return { type, parent, string };
} else {
const string = tag.comment;
return { type, string };
}
}
});
}
function traverse(sourceFile: ts.SourceFile, node: ts.Node, comments: any[], parentContext) {
const jsDocs: any[] = (node as any).jsDoc;
const jsDoc = jsDocs && jsDocs.length > 0 && jsDocs[jsDocs.length - 1];
let startPos: number;
if (jsDoc) {
startPos = jsDoc.pos;
} else {
startPos = node.pos;
}
let codeStartPos: number;
if (jsDoc) {
codeStartPos = jsDoc.end + 1;
} else {
codeStartPos = node.pos;
}
const line = ts.getLineAndCharacterOfPosition(sourceFile, startPos).line + 1;
const codeStart = ts.getLineAndCharacterOfPosition(sourceFile, codeStartPos).line + 1;
const description = { full: '', summary: '', body: '' };
if (jsDoc) {
description.full = jsDoc.comment || '';
description.summary = description.full.split('\n\n')[0];
description.body = description.full.split('\n\n').slice(1).join('\n\n');
}
if (jsDocs && jsDocs.length > 1) {
for (const doc of jsDocs.slice(0, jsDocs.length - 1)) {
const comment = {
code: '', codeStart: 1, ctx: undefined, description: { full: '', summary: '', body: '' },
isClass: false, isConstructor: false, isEvent: false, isPrivate: false,
line: 1, tags: getTags(doc),
};
comments.push(comment);
}
}
if (ts.isSourceFile(node)) {
for (const statement of node.statements) {
traverse(sourceFile, statement, comments, parentContext);
}
} else if (ts.isClassDeclaration(node)) {
const name = sourceFile.text.substring(node.name.pos, node.name.end).trim();
const ctx = {
cons: name,
constructor: name,
extends: '',
name,
string: `new ${name}()`,
type: 'class',
};
const comment = {
code: node.getText(sourceFile), codeStart, ctx, description,
isClass: true, isConstructor: false, isEvent: false, isPrivate: false,
line, tags: getTags(jsDoc),
};
comments.push(comment);
for (const member of node.members) {
traverse(sourceFile, member, comments, comment.ctx);
}
} else if (ts.isConstructorDeclaration(node)) {
const ctx = {
cons: parentContext.name,
constructor: parentContext.name,
is_constructor: true,
name: 'constructor',
string: `${parentContext.name}.prototype.constructor()`,
type: 'method',
};
const comment = {
code: node.getText(sourceFile), codeStart, ctx, description,
isClass: false, isConstructor: false, isEvent: false, isPrivate: false,
line, tags: getTags(jsDoc),
};
comments.push(comment);
} else if (ts.isMethodDeclaration(node) || ts.isMethodSignature(node)) {
const name = sourceFile.text.substring(Math.max(codeStartPos, node.name.pos), node.name.end).trim();
const modifierFlags = ts.getCombinedModifierFlags(node);
const isPrivate = (modifierFlags & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) !== 0;
const isStatic = (modifierFlags & ts.ModifierFlags.Static) !== 0;
let ctx;
if (isStatic) {
ctx = {
name,
receiver: parentContext.name,
string: `${parentContext.name}.${name}()`,
type: 'method',
};
} else {
ctx = {
cons: parentContext.name,
constructor: parentContext.name,
name,
string: `${parentContext.name}.prototype.${name}()`,
type: 'method',
};
}
const comment = {
code: node.getText(sourceFile), codeStart, ctx, description,
isClass: false, isConstructor: false, isEvent: false, isPrivate,
line, tags: getTags(jsDoc),
};
comments.push(comment);
} else if (ts.isPropertyDeclaration(node) || ts.isPropertySignature(node)) {
const name = sourceFile.text.substring(Math.max(codeStartPos, node.name.pos), node.name.end).trim();
const modifierFlags = ts.getCombinedModifierFlags(node);
const isPrivate = (modifierFlags & (ts.ModifierFlags.Private | ts.ModifierFlags.Protected)) !== 0;
const isStatic = (modifierFlags & ts.ModifierFlags.Static) !== 0;
let ctx;
if (isStatic) {
ctx = {
name,
receiver: parentContext.name,
string: `${parentContext.name}.${name}()`,
type: 'property',
};
} else {
ctx = {
cons: parentContext.name,
constructor: parentContext.name,
name,
string: `${parentContext.name}.prototype.${name}()`,
type: 'property',
};
}
const comment = {
code: node.getText(sourceFile), codeStart, ctx, description,
isClass: false, isConstructor: false, isEvent: false, isPrivate,
line, tags: getTags(jsDoc),
};
comments.push(comment);
} else if (ts.isFunctionDeclaration(node)) {
const name = sourceFile.text.substring(node.name.pos, node.name.end).trim();
const ctx = {
name,
string: `${name}()`,
type: 'function',
};
const comment = {
code: node.getText(sourceFile), codeStart, ctx, description,
isClass: false, isConstructor: false, isEvent: false, isPrivate: false,
line, tags: getTags(jsDoc),
};
comments.push(comment);
} else if (ts.isExpressionStatement(node)) {
const expression = node.expression;
let ctx;
if (ts.isBinaryExpression(expression)) {
const isFunc = ts.isFunctionExpression(expression.right);
const left = expression.left;
if (ts.isPropertyAccessExpression(left)) {
if (ts.isIdentifier(left.expression)) {
ctx = {
name: left.name.text,
receiver: left.expression.text,
string: `${left.expression.text}.${left.name.text}()`,
type: isFunc ? 'method' : 'property',
};
} else if (ts.isPropertyAccessExpression(left.expression)) {
const leftExpr = left.expression;
if (leftExpr.name.text === 'prototype' && ts.isIdentifier(leftExpr.expression)) {
ctx = {
cons: leftExpr.expression.text,
constructor: leftExpr.expression.text,
name: left.name.text,
string: `${leftExpr.expression.text}.prototype.${left.name.text}()`,
type: isFunc ? 'method' : 'property',
};
}
}
}
}
if (ctx) {
const comment = {
code: node.getText(sourceFile), codeStart, ctx, description,
isClass: false, isConstructor: false, isEvent: false, isPrivate: false,
line, tags: getTags(jsDoc),
};
comments.push(comment);
}
} else if (ts.isInterfaceDeclaration(node)) {
const name = sourceFile.text.substring(node.name.pos, node.name.end).trim();
const ctx = {
cons: name,
constructor: name,
extends: '',
name,
string: `new ${name}()`,
type: 'class',
};
const comment = {
class_code: node.getText(sourceFile), class_codeStart: codeStart, ctx, description,
isClass: true, isConstructor: false, isEvent: false, isPrivate: false, isStatic: true,
line, tags: getTags(jsDoc),
};
comments.push(comment);
for (const member of node.members) {
traverse(sourceFile, member, comments, comment.ctx);
}
}
}
export function parseCommentsTS(data: string) {
const comments = [];
const compilerOptions = { strict: true, target: ts.ScriptTarget.ES2017 };
const compilerHost = ts.createCompilerHost(compilerOptions, true);
compilerHost.getSourceFile = (filename: string, languageVersion: ts.ScriptTarget) => {
const text = filename === 'source.ts' ? data : ts.sys.readFile(filename);
return ts.createSourceFile(filename, text, languageVersion);
};
const program = ts.createProgram(['source.ts'], compilerOptions, compilerHost);
const sourceFile = program.getSourceFile('source.ts');
traverse(sourceFile, sourceFile, comments, null);
return comments;
}