feat: add TypeScript language parser
This commit is contained in:
227
packages/parser/src/languages/typescript.ts
Normal file
227
packages/parser/src/languages/typescript.ts
Normal 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,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user