11import * as path from 'node:path' ;
22import {
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