diff --git a/packages/parser/src/languages/typescript.ts b/packages/parser/src/languages/typescript.ts new file mode 100644 index 0000000..c184360 --- /dev/null +++ b/packages/parser/src/languages/typescript.ts @@ -0,0 +1,227 @@ +import { parse as babelParse } from "@babel/parser"; +import _traverse from "@babel/traverse"; +import type { + FileNode, + FunctionNode, + ClassNode, + ImportNode, + ExportNode, +} from "@codeboard/shared"; +import type { LanguageParser } from "./base.js"; + +const traverse = + typeof _traverse === "function" + ? _traverse + : (_traverse as unknown as { default: typeof _traverse }).default; + +function extractFunctionParams( + params: Array<{ name?: string; left?: { name?: string }; type?: string }> +): string[] { + return params.map((p) => { + if (p.type === "AssignmentPattern" && p.left?.name) return p.left.name; + return p.name ?? "unknown"; + }); +} + +export const typescriptParser: LanguageParser = { + extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"], + + parse(content: string, filePath: string): FileNode { + const functions: FunctionNode[] = []; + const classes: ClassNode[] = []; + const imports: ImportNode[] = []; + const exports: ExportNode[] = []; + const calls: Set = new Set(); + + let ast; + try { + ast = babelParse(content, { + sourceType: "module", + plugins: [ + "typescript", + "jsx", + "decorators-legacy", + "classProperties", + "classPrivateProperties", + "classPrivateMethods", + "optionalChaining", + "nullishCoalescingOperator", + "dynamicImport", + ], + errorRecovery: true, + }); + } catch { + return { + path: filePath, + language: filePath.endsWith(".py") ? "python" : "typescript", + size: content.length, + functions: [], + classes: [], + imports: [], + exports: [], + complexity: 0, + }; + } + + traverse(ast, { + FunctionDeclaration(path) { + const node = path.node; + if (!node.id) return; + functions.push({ + name: node.id.name, + params: extractFunctionParams(node.params as never[]), + returnType: node.returnType + ? content.slice(node.returnType.start!, node.returnType.end!) + : undefined, + lineStart: node.loc?.start.line ?? 0, + lineEnd: node.loc?.end.line ?? 0, + calls: [], + }); + }, + + ArrowFunctionExpression(path) { + const parent = path.parent; + if ( + parent.type === "VariableDeclarator" && + parent.id.type === "Identifier" + ) { + const node = path.node; + functions.push({ + name: parent.id.name, + params: extractFunctionParams(node.params as never[]), + returnType: node.returnType + ? content.slice(node.returnType.start!, node.returnType.end!) + : undefined, + lineStart: node.loc?.start.line ?? 0, + lineEnd: node.loc?.end.line ?? 0, + calls: [], + }); + } + }, + + ClassDeclaration(path) { + const node = path.node; + if (!node.id) return; + const methods: FunctionNode[] = []; + const properties: Array<{ name: string; type?: string }> = []; + + for (const member of node.body.body) { + if ( + member.type === "ClassMethod" && + member.key.type === "Identifier" + ) { + methods.push({ + name: member.key.name, + params: extractFunctionParams(member.params as never[]), + lineStart: member.loc?.start.line ?? 0, + lineEnd: member.loc?.end.line ?? 0, + calls: [], + }); + } else if ( + member.type === "ClassProperty" && + member.key.type === "Identifier" + ) { + properties.push({ + name: member.key.name, + type: member.typeAnnotation + ? content.slice( + member.typeAnnotation.start!, + member.typeAnnotation.end! + ) + : undefined, + }); + } + } + + classes.push({ name: node.id.name, methods, properties }); + }, + + ImportDeclaration(path) { + const node = path.node; + const specifiers = node.specifiers.map((s) => s.local.name); + imports.push({ source: node.source.value, specifiers }); + }, + + ExportDefaultDeclaration() { + exports.push({ name: "default", isDefault: true }); + }, + + ExportNamedDeclaration(path) { + const node = path.node; + if (node.declaration) { + if ( + node.declaration.type === "FunctionDeclaration" && + node.declaration.id + ) { + exports.push({ + name: node.declaration.id.name, + isDefault: false, + }); + } else if ( + node.declaration.type === "ClassDeclaration" && + node.declaration.id + ) { + exports.push({ + name: node.declaration.id.name, + isDefault: false, + }); + } else if (node.declaration.type === "VariableDeclaration") { + for (const decl of node.declaration.declarations) { + if (decl.id.type === "Identifier") { + exports.push({ name: decl.id.name, isDefault: false }); + } + } + } + } + if (node.specifiers) { + for (const spec of node.specifiers) { + if (spec.exported.type === "Identifier") { + exports.push({ name: spec.exported.name, isDefault: false }); + } + } + } + }, + + CallExpression(path) { + const callee = path.node.callee; + if (callee.type === "Identifier") { + calls.add(callee.name); + } else if ( + callee.type === "MemberExpression" && + callee.property.type === "Identifier" + ) { + calls.add(callee.property.name); + } + }, + }); + + for (const fn of functions) { + fn.calls = Array.from(calls); + } + + let complexity = 0; + traverse(ast, { + IfStatement() { complexity++; }, + ForStatement() { complexity++; }, + ForInStatement() { complexity++; }, + ForOfStatement() { complexity++; }, + WhileStatement() { complexity++; }, + DoWhileStatement() { complexity++; }, + SwitchCase() { complexity++; }, + ConditionalExpression() { complexity++; }, + LogicalExpression() { complexity++; }, + CatchClause() { complexity++; }, + }); + + return { + path: filePath, + language: filePath.match(/\.tsx?$/) ? "typescript" : "javascript", + size: content.length, + functions, + classes, + imports, + exports, + complexity, + }; + }, +};