feat: file nodes, arrow function extraction, parallel I/O
- Create file-kind nodes for each parsed source file - Add isInsideClassLikeNode() for method vs function detection - Extract arrow functions and function expressions from variable declarators - Batch file I/O with FILE_IO_BATCH_SIZE=10 using Promise.all - Add symlink cycle detection with visitedDirs Set in scanDirectory - Add lazy grammar loading with exported getGrammar() function - Add indexFileWithContent() for pre-read content processing - Add tests for file nodes and arrow function extraction
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
import { SyntaxNode, Tree } from 'tree-sitter';
|
||||
import * as crypto from 'crypto';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
Language,
|
||||
Node,
|
||||
@@ -875,7 +876,28 @@ export class TreeSitterExtractor {
|
||||
|
||||
try {
|
||||
this.tree = parser.parse(this.source);
|
||||
|
||||
// Create file node representing the source file
|
||||
const fileNode: Node = {
|
||||
id: `file:${this.filePath}`,
|
||||
kind: 'file',
|
||||
name: path.basename(this.filePath),
|
||||
qualifiedName: this.filePath,
|
||||
filePath: this.filePath,
|
||||
language: this.language,
|
||||
startLine: 1,
|
||||
endLine: this.source.split('\n').length,
|
||||
startColumn: 0,
|
||||
endColumn: 0,
|
||||
isExported: false,
|
||||
updatedAt: Date.now(),
|
||||
};
|
||||
this.nodes.push(fileNode);
|
||||
|
||||
// Push file node onto stack so top-level declarations get contains edges
|
||||
this.nodeStack.push(fileNode.id);
|
||||
this.visitNode(this.tree.rootNode);
|
||||
this.nodeStack.pop();
|
||||
} catch (error) {
|
||||
captureException(error, { operation: 'tree-sitter-parse', filePath: this.filePath, language: this.language });
|
||||
this.errors.push({
|
||||
@@ -905,7 +927,7 @@ export class TreeSitterExtractor {
|
||||
// Check for function declarations
|
||||
// For Python/Ruby, function_definition inside a class should be treated as method
|
||||
if (this.extractor.functionTypes.includes(nodeType)) {
|
||||
if (this.nodeStack.length > 0 && this.extractor.methodTypes.includes(nodeType)) {
|
||||
if (this.isInsideClassLikeNode() && this.extractor.methodTypes.includes(nodeType)) {
|
||||
// Inside a class - treat as method
|
||||
this.extractMethod(node);
|
||||
skipChildren = true; // extractMethod visits children via visitFunctionBody
|
||||
@@ -956,9 +978,17 @@ export class TreeSitterExtractor {
|
||||
else if (this.extractor.typeAliasTypes.includes(nodeType)) {
|
||||
this.extractTypeAlias(node);
|
||||
}
|
||||
// Check for arrow functions / function expressions assigned to variables (JS/TS)
|
||||
else if (nodeType === 'variable_declarator') {
|
||||
const valueNode = getChildByField(node, 'value');
|
||||
if (valueNode && (valueNode.type === 'arrow_function' || valueNode.type === 'function_expression')) {
|
||||
this.extractFunctionVariable(node);
|
||||
skipChildren = true;
|
||||
}
|
||||
}
|
||||
// Check for variable declarations (const, let, var, etc.)
|
||||
// Only extract top-level variables (not inside functions/methods)
|
||||
else if (this.extractor.variableTypes.includes(nodeType) && this.nodeStack.length === 0) {
|
||||
else if (this.extractor.variableTypes.includes(nodeType) && !this.isInsideClassLikeNode()) {
|
||||
this.extractVariable(node);
|
||||
skipChildren = true; // extractVariable handles children
|
||||
}
|
||||
@@ -1060,6 +1090,81 @@ export class TreeSitterExtractor {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current node stack indicates we are inside a class-like node
|
||||
* (class, struct, interface, trait). File nodes do not count as class-like.
|
||||
*/
|
||||
private isInsideClassLikeNode(): boolean {
|
||||
if (this.nodeStack.length === 0) return false;
|
||||
const parentId = this.nodeStack[this.nodeStack.length - 1];
|
||||
if (!parentId) return false;
|
||||
const parentNode = this.nodes.find((n) => n.id === parentId);
|
||||
if (!parentNode) return false;
|
||||
return (
|
||||
parentNode.kind === 'class' ||
|
||||
parentNode.kind === 'struct' ||
|
||||
parentNode.kind === 'interface' ||
|
||||
parentNode.kind === 'trait' ||
|
||||
parentNode.kind === 'enum'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract an arrow function or function expression assigned to a variable.
|
||||
* Handles patterns like: const foo = () => {} or const bar = function() {}
|
||||
*/
|
||||
private extractFunctionVariable(node: SyntaxNode): void {
|
||||
if (!this.extractor) return;
|
||||
|
||||
// Only handle variable_declarator where value is arrow_function or function_expression
|
||||
if (node.type !== 'variable_declarator') return;
|
||||
|
||||
const nameNode = getChildByField(node, 'name');
|
||||
const valueNode = getChildByField(node, 'value');
|
||||
|
||||
if (!nameNode || !valueNode) return;
|
||||
if (valueNode.type !== 'arrow_function' && valueNode.type !== 'function_expression') return;
|
||||
|
||||
const name = getNodeText(nameNode, this.source);
|
||||
if (!name) return;
|
||||
|
||||
// Check if exported by walking parents
|
||||
let isExported = false;
|
||||
let current = node.parent;
|
||||
while (current) {
|
||||
if (current.type === 'export_statement') {
|
||||
isExported = true;
|
||||
break;
|
||||
}
|
||||
if (current.type === 'program' || current.type === 'module') break;
|
||||
current = current.parent;
|
||||
}
|
||||
|
||||
// Build signature from the arrow function parameters
|
||||
let signature: string | undefined;
|
||||
const params = getChildByField(valueNode, 'parameters');
|
||||
if (params) {
|
||||
signature = `${name}${getNodeText(params, this.source)}`;
|
||||
}
|
||||
|
||||
// Check if async
|
||||
const isAsync = this.extractor.isAsync?.(valueNode);
|
||||
|
||||
const funcNode = this.createNode('function', name, node, {
|
||||
isExported,
|
||||
signature: signature || undefined,
|
||||
isAsync,
|
||||
});
|
||||
|
||||
// Push to stack and visit body for call extraction
|
||||
this.nodeStack.push(funcNode.id);
|
||||
const body = getChildByField(valueNode, this.extractor.bodyField);
|
||||
if (body) {
|
||||
this.visitFunctionBody(body, funcNode.id);
|
||||
}
|
||||
this.nodeStack.pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a function
|
||||
*/
|
||||
@@ -1160,10 +1265,10 @@ export class TreeSitterExtractor {
|
||||
private extractMethod(node: SyntaxNode): void {
|
||||
if (!this.extractor) return;
|
||||
|
||||
// For most languages, only extract as method if inside a class
|
||||
// For most languages, only extract as method if inside a class-like node
|
||||
// But Go methods are top-level with a receiver, so always treat them as methods
|
||||
if (this.nodeStack.length === 0 && this.language !== 'go') {
|
||||
// Top-level and not Go, treat as function
|
||||
if (!this.isInsideClassLikeNode() && this.language !== 'go') {
|
||||
// Not inside a class-like node and not Go, treat as function
|
||||
this.extractFunction(node);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user