dox/dox_ts.ts

// 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;
}
Fork me on GitHub