Skip to content

Commit 5722c3c

Browse files
committed
feat(core): extract callees from functions and methods
Use ts-morph to extract call relationships during scanning: - extractCallees() finds all CallExpression nodes within a function/method - extractCalleeFromExpression() extracts name, line, and resolved file path - Handles: function calls, method calls, chained calls, constructor calls - Deduplicates by name+line to avoid repeated calls - Resolves file paths via symbol lookup (excludes node_modules) - Only populates callees field when calls exist Part of #80
1 parent fd3983a commit 5722c3c

File tree

1 file changed

+106
-5
lines changed

1 file changed

+106
-5
lines changed

packages/core/src/scanner/typescript.ts

Lines changed: 106 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as path from 'node:path';
22
import {
3+
type CallExpression,
34
type ClassDeclaration,
45
type FunctionDeclaration,
56
type InterfaceDeclaration,
@@ -10,7 +11,7 @@ import {
1011
SyntaxKind,
1112
type TypeAliasDeclaration,
1213
} from 'ts-morph';
13-
import type { Document, Scanner, ScannerCapabilities } from './types';
14+
import type { CalleeInfo, Document, Scanner, ScannerCapabilities } from './types';
1415

1516
/**
1617
* Enhanced TypeScript scanner using ts-morph
@@ -80,7 +81,7 @@ export class TypeScriptScanner implements Scanner {
8081

8182
// Extract functions
8283
for (const fn of sourceFile.getFunctions()) {
83-
const doc = this.extractFunction(fn, relativeFile, imports);
84+
const doc = this.extractFunction(fn, relativeFile, imports, sourceFile);
8485
if (doc) documents.push(doc);
8586
}
8687

@@ -95,7 +96,8 @@ export class TypeScriptScanner implements Scanner {
9596
method,
9697
cls.getName() || 'Anonymous',
9798
relativeFile,
98-
imports
99+
imports,
100+
sourceFile
99101
);
100102
if (methodDoc) documents.push(methodDoc);
101103
}
@@ -143,7 +145,8 @@ export class TypeScriptScanner implements Scanner {
143145
private extractFunction(
144146
fn: FunctionDeclaration,
145147
file: string,
146-
imports: string[]
148+
imports: string[],
149+
sourceFile: SourceFile
147150
): Document | null {
148151
const name = fn.getName();
149152
if (!name) return null; // Skip anonymous functions
@@ -155,6 +158,7 @@ export class TypeScriptScanner implements Scanner {
155158
const docComment = this.getDocComment(fn);
156159
const isExported = fn.isExported();
157160
const snippet = this.truncateSnippet(fullText);
161+
const callees = this.extractCallees(fn, sourceFile);
158162

159163
// Build text for embedding
160164
const text = this.buildEmbeddingText({
@@ -180,6 +184,7 @@ export class TypeScriptScanner implements Scanner {
180184
docstring: docComment,
181185
snippet,
182186
imports,
187+
callees: callees.length > 0 ? callees : undefined,
183188
},
184189
};
185190
}
@@ -234,7 +239,8 @@ export class TypeScriptScanner implements Scanner {
234239
method: MethodDeclaration,
235240
className: string,
236241
file: string,
237-
imports: string[]
242+
imports: string[],
243+
sourceFile: SourceFile
238244
): Document | null {
239245
const name = method.getName();
240246
if (!name) return null;
@@ -246,6 +252,7 @@ export class TypeScriptScanner implements Scanner {
246252
const docComment = this.getDocComment(method);
247253
const isPublic = !method.hasModifier(SyntaxKind.PrivateKeyword);
248254
const snippet = this.truncateSnippet(fullText);
255+
const callees = this.extractCallees(method, sourceFile);
249256

250257
const text = this.buildEmbeddingText({
251258
type: 'method',
@@ -270,6 +277,7 @@ export class TypeScriptScanner implements Scanner {
270277
docstring: docComment,
271278
snippet,
272279
imports,
280+
callees: callees.length > 0 ? callees : undefined,
273281
},
274282
};
275283
}
@@ -408,4 +416,97 @@ export class TypeScriptScanner implements Scanner {
408416
const remaining = lines.length - maxLines;
409417
return `${truncated}\n// ... ${remaining} more lines`;
410418
}
419+
420+
/**
421+
* Extract callees (functions/methods called) from a node
422+
* Handles: function calls, method calls, constructor calls
423+
*/
424+
private extractCallees(node: Node, sourceFile: SourceFile): CalleeInfo[] {
425+
const callees: CalleeInfo[] = [];
426+
const seenCalls = new Set<string>(); // Deduplicate by name+line
427+
428+
// Get all call expressions within this node
429+
const callExpressions = node.getDescendantsOfKind(SyntaxKind.CallExpression);
430+
431+
for (const callExpr of callExpressions) {
432+
const calleeInfo = this.extractCalleeFromExpression(callExpr, sourceFile);
433+
if (calleeInfo) {
434+
const key = `${calleeInfo.name}:${calleeInfo.line}`;
435+
if (!seenCalls.has(key)) {
436+
seenCalls.add(key);
437+
callees.push(calleeInfo);
438+
}
439+
}
440+
}
441+
442+
// Also handle new expressions (constructor calls)
443+
const newExpressions = node.getDescendantsOfKind(SyntaxKind.NewExpression);
444+
for (const newExpr of newExpressions) {
445+
const expression = newExpr.getExpression();
446+
const name = expression.getText();
447+
const line = newExpr.getStartLineNumber();
448+
const key = `new ${name}:${line}`;
449+
450+
if (!seenCalls.has(key)) {
451+
seenCalls.add(key);
452+
callees.push({
453+
name: `new ${name}`,
454+
line,
455+
file: undefined, // Could resolve via type checker if needed
456+
});
457+
}
458+
}
459+
460+
return callees;
461+
}
462+
463+
/**
464+
* Extract callee info from a call expression
465+
*/
466+
private extractCalleeFromExpression(
467+
callExpr: CallExpression,
468+
_sourceFile: SourceFile
469+
): CalleeInfo | null {
470+
const expression = callExpr.getExpression();
471+
const line = callExpr.getStartLineNumber();
472+
473+
// Handle different call patterns:
474+
// 1. Simple call: foo()
475+
// 2. Method call: obj.method()
476+
// 3. Chained call: a.b.c()
477+
// 4. Computed property: obj[key]()
478+
479+
const expressionText = expression.getText();
480+
481+
// Skip very complex expressions (e.g., IIFEs, callbacks)
482+
if (expressionText.includes('(') || expressionText.includes('=>')) {
483+
return null;
484+
}
485+
486+
// Try to resolve the definition file
487+
let file: string | undefined;
488+
try {
489+
// Get the symbol and find its declaration
490+
const symbol = expression.getSymbol();
491+
if (symbol) {
492+
const declarations = symbol.getDeclarations();
493+
if (declarations.length > 0) {
494+
const declSourceFile = declarations[0].getSourceFile();
495+
const filePath = declSourceFile.getFilePath();
496+
// Only include if it's within the project (not node_modules)
497+
if (!filePath.includes('node_modules')) {
498+
file = filePath;
499+
}
500+
}
501+
}
502+
} catch {
503+
// Symbol resolution can fail for various reasons, continue without file
504+
}
505+
506+
return {
507+
name: expressionText,
508+
line,
509+
file,
510+
};
511+
}
411512
}

0 commit comments

Comments
 (0)