@@ -35,6 +35,9 @@ def parse_file(self, file_path: str, content: str) -> Tuple[Dict[str, SymbolInfo
3535
3636 # Symbol lookup index for O(1) access
3737 symbol_lookup = {} # name -> symbol_id mapping
38+ pending_calls : List [Tuple [str , str ]] = []
39+ pending_call_set : Set [Tuple [str , str ]] = set ()
40+ variable_scopes : List [Dict [str , str ]] = [{}]
3841
3942 parser = tree_sitter .Parser (self .ts_language )
4043 tree = parser .parse (content .encode ('utf8' ))
@@ -48,7 +51,10 @@ def parse_file(self, file_path: str, content: str) -> Tuple[Dict[str, SymbolInfo
4851 classes = classes ,
4952 imports = imports ,
5053 exports = exports ,
51- symbol_lookup = symbol_lookup
54+ symbol_lookup = symbol_lookup ,
55+ pending_calls = pending_calls ,
56+ pending_call_set = pending_call_set ,
57+ variable_scopes = variable_scopes ,
5258 )
5359
5460 self ._traverse_node_single_pass (tree .root_node , context )
@@ -61,15 +67,20 @@ def parse_file(self, file_path: str, content: str) -> Tuple[Dict[str, SymbolInfo
6167 exports = exports
6268 )
6369
70+ if context .pending_calls :
71+ file_info .pending_calls = context .pending_calls
72+
6473 return symbols , file_info
6574
6675 def _traverse_node_single_pass (self , node , context : 'TraversalContext' ,
6776 current_function : Optional [str ] = None ,
6877 current_class : Optional [str ] = None ):
6978 """Single-pass traversal that extracts symbols and analyzes calls."""
7079
80+ node_type = node .type
81+
7182 # Handle function declarations
72- if node . type == 'function_declaration' :
83+ if node_type == 'function_declaration' :
7384 name = self ._get_function_name (node , context .content )
7485 if name :
7586 symbol_id = self ._create_symbol_id (context .file_path , name )
@@ -92,7 +103,7 @@ def _traverse_node_single_pass(self, node, context: 'TraversalContext',
92103 return
93104
94105 # Handle class declarations
95- elif node . type == 'class_declaration' :
106+ elif node_type == 'class_declaration' :
96107 name = self ._get_class_name (node , context .content )
97108 if name :
98109 symbol_id = self ._create_symbol_id (context .file_path , name )
@@ -112,7 +123,7 @@ def _traverse_node_single_pass(self, node, context: 'TraversalContext',
112123 return
113124
114125 # Handle interface declarations
115- elif node . type == 'interface_declaration' :
126+ elif node_type == 'interface_declaration' :
116127 name = self ._get_interface_name (node , context .content )
117128 if name :
118129 symbol_id = self ._create_symbol_id (context .file_path , name )
@@ -132,7 +143,7 @@ def _traverse_node_single_pass(self, node, context: 'TraversalContext',
132143 return
133144
134145 # Handle method definitions
135- elif node . type == 'method_definition' :
146+ elif node_type == 'method_definition' :
136147 method_name = self ._get_method_name (node , context .content )
137148 if method_name and current_class :
138149 full_name = f"{ current_class } .{ method_name } "
@@ -156,37 +167,68 @@ def _traverse_node_single_pass(self, node, context: 'TraversalContext',
156167 current_class = current_class )
157168 return
158169
170+ # Handle variable declarations that define callable exports
171+ elif node_type in ['lexical_declaration' , 'variable_statement' ]:
172+ handled = False
173+ for child in node .children :
174+ if child .type != 'variable_declarator' :
175+ continue
176+ name_node = child .child_by_field_name ('name' )
177+ value_node = child .child_by_field_name ('value' )
178+ if not name_node or not value_node :
179+ continue
180+
181+ if current_function is not None :
182+ continue
183+
184+ value_type = value_node .type
185+ if value_type not in [
186+ 'arrow_function' ,
187+ 'function' ,
188+ 'function_expression' ,
189+ 'call_expression' ,
190+ 'new_expression' ,
191+ 'identifier' ,
192+ 'member_expression' ,
193+ ]:
194+ continue
195+
196+ name = context .content [name_node .start_byte :name_node .end_byte ]
197+ symbol_id = self ._create_symbol_id (context .file_path , name )
198+ signature = context .content [child .start_byte :child .end_byte ].split ('\n ' )[0 ].strip ()
199+ symbol_info = SymbolInfo (
200+ type = "function" ,
201+ file = context .file_path ,
202+ line = child .start_point [0 ] + 1 ,
203+ signature = signature
204+ )
205+ context .symbols [symbol_id ] = symbol_info
206+ context .symbol_lookup [name ] = symbol_id
207+ context .functions .append (name )
208+ handled = True
209+
210+ if value_type in ['arrow_function' , 'function' , 'function_expression' ]:
211+ func_context = f"{ context .file_path } ::{ name } "
212+ context .variable_scopes .append ({})
213+ self ._traverse_node_single_pass (
214+ value_node ,
215+ context ,
216+ current_function = func_context ,
217+ current_class = current_class
218+ )
219+ context .variable_scopes .pop ()
220+
221+ if handled :
222+ return
223+
159224 # Handle function calls
160- elif node .type == 'call_expression' and current_function :
161- # Extract the function being called
162- called_function = None
163- if node .children :
164- func_node = node .children [0 ]
165- if func_node .type == 'identifier' :
166- # Direct function call
167- called_function = context .content [func_node .start_byte :func_node .end_byte ]
168- elif func_node .type == 'member_expression' :
169- # Method call (obj.method or this.method)
170- for child in func_node .children :
171- if child .type == 'property_identifier' :
172- called_function = context .content [child .start_byte :child .end_byte ]
173- break
174-
175- # Add relationship using O(1) lookup
176- if called_function :
177- if called_function in context .symbol_lookup :
178- symbol_id = context .symbol_lookup [called_function ]
179- symbol_info = context .symbols [symbol_id ]
180- if current_function not in symbol_info .called_by :
181- symbol_info .called_by .append (current_function )
182- else :
183- # Try to find method with class prefix
184- for name , sid in context .symbol_lookup .items ():
185- if name .endswith (f".{ called_function } " ):
186- symbol_info = context .symbols [sid ]
187- if current_function not in symbol_info .called_by :
188- symbol_info .called_by .append (current_function )
189- break
225+ elif node_type == 'call_expression' :
226+ caller = current_function or f"{ context .file_path } :{ node .start_point [0 ] + 1 } "
227+ called_function = self ._resolve_called_function (node , context , current_class )
228+ if caller and called_function :
229+ self ._register_call (context , caller , called_function )
230+ if caller :
231+ self ._collect_callback_arguments (node , context , caller , current_class , current_function )
190232
191233 # Handle import declarations
192234 elif node .type == 'import_statement' :
@@ -203,6 +245,185 @@ def _traverse_node_single_pass(self, node, context: 'TraversalContext',
203245 self ._traverse_node_single_pass (child , context , current_function = current_function ,
204246 current_class = current_class )
205247
248+ def _register_call (self , context : 'TraversalContext' , caller : str , called : str ) -> None :
249+ if called in context .symbol_lookup :
250+ symbol_id = context .symbol_lookup [called ]
251+ symbol_info = context .symbols [symbol_id ]
252+ if caller not in symbol_info .called_by :
253+ symbol_info .called_by .append (caller )
254+ return
255+
256+ key = (caller , called )
257+ if key not in context .pending_call_set :
258+ context .pending_call_set .add (key )
259+ context .pending_calls .append (key )
260+
261+ def _collect_callback_arguments (
262+ self ,
263+ node ,
264+ context : 'TraversalContext' ,
265+ caller : str ,
266+ current_class : Optional [str ],
267+ current_function : Optional [str ]
268+ ) -> None :
269+ arguments_node = node .child_by_field_name ('arguments' )
270+ if not arguments_node :
271+ return
272+
273+ for argument in arguments_node .children :
274+ if not getattr (argument , "is_named" , False ):
275+ continue
276+ callback_name = self ._resolve_argument_reference (argument , context , current_class )
277+ if callback_name :
278+ call_site = caller
279+ if current_function is None :
280+ call_site = f"{ context .file_path } :{ argument .start_point [0 ] + 1 } "
281+ self ._register_call (context , call_site , callback_name )
282+
283+ def _resolve_argument_reference (
284+ self ,
285+ node ,
286+ context : 'TraversalContext' ,
287+ current_class : Optional [str ]
288+ ) -> Optional [str ]:
289+ node_type = node .type
290+
291+ if node_type == 'identifier' :
292+ return context .content [node .start_byte :node .end_byte ]
293+
294+ if node_type == 'member_expression' :
295+ property_node = node .child_by_field_name ('property' )
296+ if property_node is None :
297+ for child in node .children :
298+ if child .type in ['property_identifier' , 'identifier' ]:
299+ property_node = child
300+ break
301+ if property_node is None :
302+ return None
303+
304+ property_name = context .content [property_node .start_byte :property_node .end_byte ]
305+ qualifier_node = node .child_by_field_name ('object' )
306+ qualifier = self ._resolve_member_qualifier (
307+ qualifier_node ,
308+ context ,
309+ current_class
310+ )
311+ if not qualifier :
312+ for child in node .children :
313+ if child is property_node :
314+ continue
315+ qualifier = self ._resolve_member_qualifier (
316+ child ,
317+ context ,
318+ current_class
319+ )
320+ if qualifier :
321+ break
322+ if qualifier :
323+ return f"{ qualifier } .{ property_name } "
324+ return property_name
325+
326+ return None
327+
328+ def _resolve_called_function (
329+ self ,
330+ node ,
331+ context : 'TraversalContext' ,
332+ current_class : Optional [str ]
333+ ) -> Optional [str ]:
334+ function_node = node .child_by_field_name ('function' )
335+ if function_node is None and node .children :
336+ function_node = node .children [0 ]
337+ if function_node is None :
338+ return None
339+
340+ if function_node .type == 'identifier' :
341+ return context .content [function_node .start_byte :function_node .end_byte ]
342+
343+ if function_node .type == 'member_expression' :
344+ property_node = function_node .child_by_field_name ('property' )
345+ if property_node is None :
346+ for child in function_node .children :
347+ if child .type in ['property_identifier' , 'identifier' ]:
348+ property_node = child
349+ break
350+ if property_node is None :
351+ return None
352+
353+ property_name = context .content [property_node .start_byte :property_node .end_byte ]
354+ qualifier_node = function_node .child_by_field_name ('object' )
355+ qualifier = self ._resolve_member_qualifier (
356+ qualifier_node ,
357+ context ,
358+ current_class
359+ )
360+ if not qualifier :
361+ for child in function_node .children :
362+ if child is property_node :
363+ continue
364+ qualifier = self ._resolve_member_qualifier (
365+ child ,
366+ context ,
367+ current_class
368+ )
369+ if qualifier :
370+ break
371+ if qualifier :
372+ return f"{ qualifier } .{ property_name } "
373+ return property_name
374+
375+ return None
376+
377+ def _resolve_member_qualifier (
378+ self ,
379+ node ,
380+ context : 'TraversalContext' ,
381+ current_class : Optional [str ]
382+ ) -> Optional [str ]:
383+ if node is None :
384+ return None
385+
386+ node_type = node .type
387+ if node_type == 'this' :
388+ return current_class
389+
390+ if node_type == 'identifier' :
391+ return context .content [node .start_byte :node .end_byte ]
392+
393+ if node_type == 'member_expression' :
394+ property_node = node .child_by_field_name ('property' )
395+ if property_node is None :
396+ for child in node .children :
397+ if child .type in ['property_identifier' , 'identifier' ]:
398+ property_node = child
399+ break
400+ if property_node is None :
401+ return None
402+
403+ qualifier = self ._resolve_member_qualifier (
404+ node .child_by_field_name ('object' ),
405+ context ,
406+ current_class
407+ )
408+ if not qualifier :
409+ for child in node .children :
410+ if child is property_node :
411+ continue
412+ qualifier = self ._resolve_member_qualifier (
413+ child ,
414+ context ,
415+ current_class
416+ )
417+ if qualifier :
418+ break
419+
420+ property_name = context .content [property_node .start_byte :property_node .end_byte ]
421+ if qualifier :
422+ return f"{ qualifier } .{ property_name } "
423+ return property_name
424+
425+ return None
426+
206427 def _get_function_name (self , node , content : str ) -> Optional [str ]:
207428 """Extract function name from tree-sitter node."""
208429 for child in node .children :
@@ -239,13 +460,28 @@ def _get_ts_function_signature(self, node, content: str) -> str:
239460class TraversalContext :
240461 """Context object to pass state during single-pass traversal."""
241462
242- def __init__ (self , content : str , file_path : str , symbols : Dict ,
243- functions : List , classes : List , imports : List , exports : List , symbol_lookup : Dict ):
463+ def __init__ (
464+ self ,
465+ content : str ,
466+ file_path : str ,
467+ symbols : Dict ,
468+ functions : List ,
469+ classes : List ,
470+ imports : List ,
471+ exports : List ,
472+ symbol_lookup : Dict ,
473+ pending_calls : List [Tuple [str , str ]],
474+ pending_call_set : Set [Tuple [str , str ]],
475+ variable_scopes : List [Dict [str , str ]],
476+ ):
244477 self .content = content
245478 self .file_path = file_path
246479 self .symbols = symbols
247480 self .functions = functions
248481 self .classes = classes
249482 self .imports = imports
250483 self .exports = exports
251- self .symbol_lookup = symbol_lookup
484+ self .symbol_lookup = symbol_lookup
485+ self .pending_calls = pending_calls
486+ self .pending_call_set = pending_call_set
487+ self .variable_scopes = variable_scopes
0 commit comments