#!/usr/bin/env node // Usage: node parse_ts.js file.ts const fs = require('fs'); const path = require('path'); const { parse } = require('@typescript-eslint/typescript-estree'); function getLoc(node) { if (node.loc) { return { start: node.loc.start.line, end: node.loc.end.line }; } return { start: 1, end: 1 }; } function extractLeadingComment(node, sourceCode) { if (!node.range || !sourceCode) return ""; const startIdx = node.range[0]; let commentEnd = startIdx; // Look backward for comments let i = startIdx - 1; let commentLines = []; let inBlock = false; while (i >= 0) { const char = sourceCode[i]; if (char === '\n') break; i--; } const lineStart = i + 1; const lineAbove = sourceCode.slice(lineStart, startIdx).trim(); // Check for // comment on same line before node if (lineAbove.startsWith('//')) { return lineAbove.substring(2).trim(); } // Look further up for multi-line or JSDoc const lines = sourceCode.substring(0, lineStart).split('\n'); for (let j = lines.length - 1; j >= Math.max(0, lines.length - 5); j--) { const line = lines[j].trim(); if (line.startsWith('//')) { commentLines.unshift(line.substring(2).trim()); } else if (line.endsWith('*/')) { inBlock = true; commentLines.unshift(line.slice(0, -2).trim()); } else if (inBlock) { if (line.startsWith('/*') || line.startsWith('/**')) { commentLines.unshift(line.slice(2).trim()); break; } else { commentLines.unshift(line); } } else if (line === '') { if (commentLines.length > 0) continue; else break; } else { break; } } return commentLines.join('\n').trim(); } function extractDeclarations(ast, sourceCode) { const decls = []; function visit(node) { if (!node || typeof node !== 'object') return; if (Array.isArray(node)) { node.forEach(n => visit(n)); return; } let docComment = extractLeadingComment(node, sourceCode); if (node.type === 'FunctionDeclaration' && node.id?.name) { const loc = getLoc(node); decls.push({ name: node.id.name, type: 'function', doc_comment: docComment, start_line: loc.start, end_line: loc.end }); } else if ( node.type === 'VariableDeclarator' && node.id?.type === 'Identifier' && node.init?.type === 'ArrowFunctionExpression' ) { const loc = getLoc(node); decls.push({ name: node.id.name, type: 'function', doc_comment: docComment, start_line: loc.start, end_line: loc.end }); } else if (node.type === 'ClassDeclaration' && node.id?.name) { const loc = getLoc(node); decls.push({ name: node.id.name, type: 'class', doc_comment: docComment, start_line: loc.start, end_line: loc.end }); } else if (node.type === 'TSInterfaceDeclaration' && node.id?.name) { const loc = getLoc(node); decls.push({ name: node.id.name, type: 'interface', doc_comment: docComment, start_line: loc.start, end_line: loc.end }); } else if (node.type === 'TSTypeAliasDeclaration' && node.id?.name) { const loc = getLoc(node); decls.push({ name: node.id.name, type: 'type', doc_comment: docComment, start_line: loc.start, end_line: loc.end }); } else if ( node.type === 'VariableDeclarator' && node.id?.type === 'Identifier' ) { const parent = node.parent; if ( parent?.type === 'VariableDeclaration' && ['const', 'let'].includes(parent.kind) ) { const loc = getLoc(node); decls.push({ name: node.id.name, type: 'variable', doc_comment: docComment, start_line: loc.start, end_line: loc.end }); } } Object.values(node).forEach(visit); } visit(ast); return decls; } if (process.argv.length < 3) { console.error('Usage: node parse_ts.js '); process.exit(1); } const filePath = path.resolve(process.argv[2]); try { const code = fs.readFileSync(filePath, 'utf8'); const ast = parse(code, { sourceType: 'module', loc: true, range: true, comment: false // we extract manually for simplicity }); const decls = extractDeclarations(ast, code); console.log(JSON.stringify(decls, null, 2)); } catch (e) { console.error(`ERR: ${e.message}`); process.exit(2); }