feat: add TypeScript language parser

This commit is contained in:
2001-01-01 00:00:00 +00:00
parent f8a4eab76a
commit 27832c6eaa

View File

@@ -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<string> = 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,
};
},
};