Files
2025-11-15 02:23:20 +00:00

504 lines
13 KiB
JavaScript
Executable File

#!/usr/bin/env node
// Enhanced TypeScript/JavaScript AST parser
// 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];
// Find the start of the line containing the node
let lineStart = startIdx;
while (lineStart > 0 && sourceCode[lineStart - 1] !== "\n") {
lineStart--;
}
// Check for inline comment on the same line
const lineContent = sourceCode.slice(lineStart, startIdx).trim();
if (lineContent.startsWith("//")) {
return lineContent.substring(2).trim();
}
// Look backward through previous lines for comments
const lines = sourceCode.substring(0, lineStart).split("\n");
const commentLines = [];
let inBlockComment = false;
for (let i = lines.length - 1; i >= Math.max(0, lines.length - 10); i--) {
const line = lines[i].trim();
if (line.endsWith("*/")) {
// End of block comment found
inBlockComment = true;
const blockContent = line
.slice(0, -2)
.replace(/^\*+\s*/, "")
.trim();
if (blockContent) {
commentLines.unshift(blockContent);
}
} else if (inBlockComment) {
if (line.startsWith("/*") || line.startsWith("/**")) {
// Start of block comment found
const blockContent = line
.substring(line.indexOf("/*") + 2)
.replace(/^\*+\s*/, "")
.trim();
if (blockContent) {
commentLines.unshift(blockContent);
}
break;
} else {
// Inside block comment
const blockContent = line.replace(/^\*+\s*/, "").trim();
if (blockContent) {
commentLines.unshift(blockContent);
}
}
} else if (line.startsWith("//")) {
// Single-line comment
commentLines.unshift(line.substring(2).trim());
} else if (line === "") {
// Empty line - continue if we already have comments
if (commentLines.length === 0) {
break;
}
} else {
// Non-comment, non-empty line - stop looking
break;
}
}
return commentLines.join("\n").trim();
}
function extractTypeInfo(node) {
const typeInfo = {};
// Extract type annotation
if (node.typeAnnotation) {
typeInfo.returnType = extractTypeString(node.typeAnnotation.typeAnnotation);
}
// Extract parameters for functions
if (node.params) {
typeInfo.parameters = node.params.map((param) => {
const paramInfo = {
name: extractParamName(param),
type: param.typeAnnotation
? extractTypeString(param.typeAnnotation.typeAnnotation)
: "any",
};
if (param.optional) {
paramInfo.optional = true;
}
return paramInfo;
});
}
// Extract generics
if (node.typeParameters) {
typeInfo.generics = node.typeParameters.params.map((tp) => tp.name.name);
}
return typeInfo;
}
function extractParamName(param) {
if (param.type === "Identifier") {
return param.name;
}
if (param.type === "RestElement" && param.argument.type === "Identifier") {
return "..." + param.argument.name;
}
if (param.type === "AssignmentPattern" && param.left.type === "Identifier") {
return param.left.name;
}
return "unknown";
}
function extractTypeString(typeNode) {
if (!typeNode) return "any";
switch (typeNode.type) {
case "TSStringKeyword":
return "string";
case "TSNumberKeyword":
return "number";
case "TSBooleanKeyword":
return "boolean";
case "TSAnyKeyword":
return "any";
case "TSVoidKeyword":
return "void";
case "TSUndefinedKeyword":
return "undefined";
case "TSNullKeyword":
return "null";
case "TSTypeReference":
if (typeNode.typeName.type === "Identifier") {
let typeStr = typeNode.typeName.name;
if (typeNode.typeParameters) {
const params = typeNode.typeParameters.params
.map(extractTypeString)
.join(", ");
typeStr += `<${params}>`;
}
return typeStr;
}
return "unknown";
case "TSArrayType":
return extractTypeString(typeNode.elementType) + "[]";
case "TSUnionType":
return typeNode.types.map(extractTypeString).join(" | ");
case "TSIntersectionType":
return typeNode.types.map(extractTypeString).join(" & ");
case "TSFunctionType":
return "Function";
case "TSTypeLiteral":
return "object";
default:
return "unknown";
}
}
function extractDeclarations(ast, sourceCode) {
const decls = [];
const parentMap = new WeakMap();
function setParents(node, parent = null) {
if (!node || typeof node !== "object") return;
if (Array.isArray(node)) {
node.forEach((n) => setParents(n, parent));
return;
}
parentMap.set(node, parent);
Object.values(node).forEach((child) => setParents(child, node));
}
setParents(ast);
function visit(node) {
if (!node || typeof node !== "object") return;
if (Array.isArray(node)) {
node.forEach((n) => visit(n));
return;
}
const docComment = extractLeadingComment(node, sourceCode);
const loc = getLoc(node);
// Function Declarations
if (node.type === "FunctionDeclaration" && node.id?.name) {
const typeInfo = extractTypeInfo(node);
decls.push({
name: node.id.name,
type: "function",
doc_comment: docComment,
start_line: loc.start,
end_line: loc.end,
is_async: node.async || false,
is_generator: node.generator || false,
...typeInfo,
});
}
// Arrow Functions assigned to variables
else if (
node.type === "VariableDeclarator" &&
node.id?.type === "Identifier" &&
node.init?.type === "ArrowFunctionExpression"
) {
const typeInfo = extractTypeInfo(node.init);
decls.push({
name: node.id.name,
type: "function",
doc_comment: docComment,
start_line: loc.start,
end_line: loc.end,
is_async: node.init.async || false,
is_arrow: true,
...typeInfo,
});
}
// Class Declarations
else if (node.type === "ClassDeclaration" && node.id?.name) {
const classInfo = {
name: node.id.name,
type: "class",
doc_comment: docComment,
start_line: loc.start,
end_line: loc.end,
is_abstract: node.abstract || false,
};
// Extract extends
if (node.superClass) {
if (node.superClass.type === "Identifier") {
classInfo.extends = node.superClass.name;
}
}
// Extract implements
if (node.implements && node.implements.length > 0) {
classInfo.implements = node.implements.map(
(impl) => impl.expression.name || "unknown",
);
}
// Extract generics
if (node.typeParameters) {
classInfo.generics = node.typeParameters.params.map(
(tp) => tp.name.name,
);
}
// Extract methods and properties
const methods = [];
const properties = [];
if (node.body && node.body.body) {
for (const member of node.body.body) {
if (member.type === "MethodDefinition" && member.key?.name) {
methods.push(member.key.name);
} else if (member.type === "PropertyDefinition" && member.key?.name) {
properties.push(member.key.name);
}
}
}
if (methods.length > 0) {
classInfo.methods = methods;
}
if (properties.length > 0) {
classInfo.properties = properties;
}
decls.push(classInfo);
}
// Interface Declarations
else if (node.type === "TSInterfaceDeclaration" && node.id?.name) {
const interfaceInfo = {
name: node.id.name,
type: "interface",
doc_comment: docComment,
start_line: loc.start,
end_line: loc.end,
};
// Extract extends
if (node.extends && node.extends.length > 0) {
interfaceInfo.extends = node.extends.map(
(ext) => ext.expression.name || "unknown",
);
}
// Extract generics
if (node.typeParameters) {
interfaceInfo.generics = node.typeParameters.params.map(
(tp) => tp.name.name,
);
}
// Extract properties/methods
const properties = [];
const methods = [];
if (node.body && node.body.body) {
for (const member of node.body.body) {
if (member.type === "TSPropertySignature" && member.key?.name) {
properties.push(member.key.name);
} else if (member.type === "TSMethodSignature" && member.key?.name) {
methods.push(member.key.name);
}
}
}
if (properties.length > 0) {
interfaceInfo.properties = properties;
}
if (methods.length > 0) {
interfaceInfo.methods = methods;
}
decls.push(interfaceInfo);
}
// Type Alias Declarations
else if (node.type === "TSTypeAliasDeclaration" && node.id?.name) {
const typeInfo = {
name: node.id.name,
type: "type",
doc_comment: docComment,
start_line: loc.start,
end_line: loc.end,
};
// Extract generics
if (node.typeParameters) {
typeInfo.generics = node.typeParameters.params.map(
(tp) => tp.name.name,
);
}
// Extract type definition
if (node.typeAnnotation) {
typeInfo.type_definition = extractTypeString(node.typeAnnotation);
}
decls.push(typeInfo);
}
// Enum Declarations
else if (node.type === "TSEnumDeclaration" && node.id?.name) {
const members = [];
if (node.members) {
for (const member of node.members) {
if (member.id?.type === "Identifier") {
members.push(member.id.name);
}
}
}
decls.push({
name: node.id.name,
type: "enum",
doc_comment: docComment,
start_line: loc.start,
end_line: loc.end,
members: members,
});
}
// Variable Declarations (const, let, var)
else if (
node.type === "VariableDeclarator" &&
node.id?.type === "Identifier"
) {
const parent = parentMap.get(node);
if (parent?.type === "VariableDeclaration") {
const varInfo = {
name: node.id.name,
type: "variable",
doc_comment: docComment,
start_line: loc.start,
end_line: loc.end,
var_kind: parent.kind, // const, let, or var
};
// Extract type annotation
if (node.id.typeAnnotation) {
varInfo.var_type = extractTypeString(
node.id.typeAnnotation.typeAnnotation,
);
}
decls.push(varInfo);
}
}
// Import Declarations
else if (node.type === "ImportDeclaration" && node.source?.value) {
const importInfo = {
name: node.source.value,
type: "import",
start_line: loc.start,
end_line: loc.end,
import_path: node.source.value,
};
// Extract imported names
const imports = [];
if (node.specifiers) {
for (const spec of node.specifiers) {
if (spec.type === "ImportSpecifier" && spec.imported?.name) {
imports.push(spec.imported.name);
} else if (
spec.type === "ImportDefaultSpecifier" &&
spec.local?.name
) {
imports.push(`default as ${spec.local.name}`);
} else if (
spec.type === "ImportNamespaceSpecifier" &&
spec.local?.name
) {
imports.push(`* as ${spec.local.name}`);
}
}
}
if (imports.length > 0) {
importInfo.imported_names = imports;
}
decls.push(importInfo);
}
// Export Declarations
else if (node.type === "ExportNamedDeclaration") {
if (node.declaration) {
// Export with declaration: export const x = ...
visit(node.declaration);
} else if (node.specifiers && node.specifiers.length > 0) {
// Named exports: export { x, y }
for (const spec of node.specifiers) {
if (spec.exported?.name) {
decls.push({
name: spec.exported.name,
type: "export",
start_line: loc.start,
end_line: loc.end,
export_kind: "named",
});
}
}
}
}
// Continue traversing
Object.values(node).forEach(visit);
}
visit(ast);
return decls;
}
// Main execution
if (process.argv.length < 3) {
console.error("Usage: node parse_ts.js <file.ts|file.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,
ecmaFeatures: {
jsx: true,
},
});
const decls = extractDeclarations(ast, code);
console.log(JSON.stringify(decls, null, 2));
} catch (e) {
console.error(`ERR: ${e.message}`);
process.exit(2);
}