504 lines
13 KiB
JavaScript
Executable File
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);
|
|
}
|