code-oracle 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. code_oracle/__init__.py +30 -0
  2. code_oracle/cli.py +795 -0
  3. code_oracle/config.py +145 -0
  4. code_oracle/dataset.py +5325 -0
  5. code_oracle/dead_code/__init__.py +32 -0
  6. code_oracle/dead_code/detector.py +379 -0
  7. code_oracle/dead_code/entrypoints.py +333 -0
  8. code_oracle/dead_code/models.py +255 -0
  9. code_oracle/dead_code/semantics.py +416 -0
  10. code_oracle/decision.py +906 -0
  11. code_oracle/engine.py +430 -0
  12. code_oracle/export_onnx.py +436 -0
  13. code_oracle/hook.py +531 -0
  14. code_oracle/indexer.py +894 -0
  15. code_oracle/languages/__init__.py +114 -0
  16. code_oracle/languages/common.py +127 -0
  17. code_oracle/languages/go.py +395 -0
  18. code_oracle/languages/python.py +336 -0
  19. code_oracle/languages/rust.py +474 -0
  20. code_oracle/languages/typescript.py +775 -0
  21. code_oracle/linearizer.py +166 -0
  22. code_oracle/locator.py +301 -0
  23. code_oracle/models.py +237 -0
  24. code_oracle/perf_lint/__init__.py +38 -0
  25. code_oracle/perf_lint/engine.py +234 -0
  26. code_oracle/perf_lint/models.py +229 -0
  27. code_oracle/perf_lint/rules/__init__.py +31 -0
  28. code_oracle/perf_lint/rules/async_blocking.py +143 -0
  29. code_oracle/perf_lint/rules/n_plus_one.py +232 -0
  30. code_oracle/perf_lint/rules/nested_loops.py +137 -0
  31. code_oracle/perf_lint/rules/unclosed_res.py +494 -0
  32. code_oracle/perf_lint/visitor.py +299 -0
  33. code_oracle/server.py +184 -0
  34. code_oracle/slicer.py +225 -0
  35. code_oracle/symbolic.py +459 -0
  36. code_oracle-0.1.0.dist-info/METADATA +225 -0
  37. code_oracle-0.1.0.dist-info/RECORD +40 -0
  38. code_oracle-0.1.0.dist-info/WHEEL +4 -0
  39. code_oracle-0.1.0.dist-info/entry_points.txt +2 -0
  40. code_oracle-0.1.0.dist-info/licenses/LICENSE +190 -0
@@ -0,0 +1,775 @@
1
+ """
2
+ TypeScript and JavaScript AST extractor using Tree-sitter.
3
+ Supports .ts, .tsx, .js, .jsx, .mjs, and .cjs files.
4
+ """
5
+
6
+ from typing import List, Optional, Set, Tuple
7
+ from tree_sitter import Language, Node, Parser
8
+ import tree_sitter_javascript
9
+ import tree_sitter_typescript
10
+
11
+ from code_oracle.languages.common import extract_preceding_docstring, format_syntax_error, get_node_text
12
+ from code_oracle.models import CallReference, ImportReference, Parameter, Symbol
13
+
14
+ _TS_LANG = Language(tree_sitter_typescript.language_typescript())
15
+ _TSX_LANG = Language(tree_sitter_typescript.language_tsx())
16
+ _JS_LANG = Language(tree_sitter_javascript.language())
17
+
18
+
19
+ def get_ts_parser(file_path: str = "") -> Parser:
20
+ """Get the appropriate Tree-sitter parser for TypeScript or JavaScript."""
21
+ lower = file_path.lower()
22
+ if lower.endswith(".tsx"):
23
+ return Parser(_TSX_LANG)
24
+ elif lower.endswith((".js", ".jsx", ".mjs", ".cjs")):
25
+ return Parser(_JS_LANG)
26
+ return Parser(_TS_LANG)
27
+
28
+
29
+ def validate_typescript_syntax(source: str, file_path: str = "") -> Optional[str]:
30
+ """Validate syntax of TypeScript/JavaScript source."""
31
+ if not source.strip():
32
+ return None
33
+ parser = get_ts_parser(file_path)
34
+ tree = parser.parse(source.encode("utf-8"))
35
+ lang_name = "TSX" if file_path.endswith(".tsx") else ("TypeScript" if file_path.endswith(".ts") else "JavaScript")
36
+ return format_syntax_error(tree.root_node, lang_name)
37
+
38
+
39
+ def _extract_parameters(params_node: Node, source_bytes: bytes) -> List[Parameter]:
40
+ """Extract parameters from formal_parameters node."""
41
+ params: List[Parameter] = []
42
+ for child in params_node.children:
43
+ if child.type in ("(", ")", ","):
44
+ continue
45
+
46
+ if child.type == "required_parameter":
47
+ param_name = ""
48
+ annotation = None
49
+ default_val = None
50
+ has_default = False
51
+ is_vararg = False
52
+ for sc in child.children:
53
+ if sc.type == "rest_pattern":
54
+ is_vararg = True
55
+ for ssc in sc.children:
56
+ if ssc.type in ("identifier", "type_identifier"):
57
+ param_name = get_node_text(ssc, source_bytes)
58
+ elif sc.type in ("identifier", "type_identifier"):
59
+ param_name = get_node_text(sc, source_bytes)
60
+ elif sc.type == "type_annotation":
61
+ annotation = get_node_text(sc, source_bytes).lstrip(": ").strip()
62
+ elif sc.type == "=":
63
+ has_default = True
64
+ elif has_default and default_val is None and sc.type not in ("=", " "):
65
+ default_val = get_node_text(sc, source_bytes).strip()
66
+
67
+ params.append(
68
+ Parameter(
69
+ name=param_name,
70
+ annotation=annotation,
71
+ default=default_val,
72
+ has_default=has_default,
73
+ is_vararg=is_vararg,
74
+ )
75
+ )
76
+
77
+ elif child.type == "optional_parameter":
78
+ param_name = ""
79
+ annotation = None
80
+ for sc in child.children:
81
+ if sc.type in ("identifier", "type_identifier"):
82
+ param_name = get_node_text(sc, source_bytes)
83
+ elif sc.type == "type_annotation":
84
+ annotation = get_node_text(sc, source_bytes).lstrip(": ").strip()
85
+
86
+ params.append(
87
+ Parameter(
88
+ name=param_name,
89
+ annotation=annotation,
90
+ has_default=True, # Optional parameter doesn't require caller argument
91
+ )
92
+ )
93
+
94
+ elif child.type == "rest_pattern":
95
+ param_name = ""
96
+ for sc in child.children:
97
+ if sc.type in ("identifier", "type_identifier"):
98
+ param_name = get_node_text(sc, source_bytes)
99
+ params.append(
100
+ Parameter(
101
+ name=param_name or "rest",
102
+ is_vararg=True,
103
+ )
104
+ )
105
+
106
+ elif child.type == "identifier":
107
+ params.append(
108
+ Parameter(
109
+ name=get_node_text(child, source_bytes),
110
+ )
111
+ )
112
+
113
+ return params
114
+
115
+
116
+ def _extract_calls(node: Node, source_bytes: bytes, caller_id: Optional[str] = None) -> List[CallReference]:
117
+ """Recursively extract function and method calls inside a node."""
118
+ calls: List[CallReference] = []
119
+
120
+ def walk(n: Node):
121
+ if n.type in ("call_expression", "new_expression"):
122
+ callee_name = ""
123
+ args_count = 0
124
+ has_vararg = False
125
+ kwargs: List[str] = []
126
+
127
+ # First child or named child 'function' / 'constructor'
128
+ fn_node = n.child_by_field_name("function") or n.child_by_field_name("constructor")
129
+ if not fn_node and n.children:
130
+ fn_node = n.children[1] if n.type == "new_expression" and len(n.children) > 1 else n.children[0]
131
+
132
+ if fn_node:
133
+ callee_name = get_node_text(fn_node, source_bytes).strip()
134
+
135
+ args_node = n.child_by_field_name("arguments")
136
+ if args_node:
137
+ for arg in args_node.children:
138
+ if arg.type in ("(", ")", ",", "comment", "line_comment", "block_comment") or "comment" in arg.type:
139
+ continue
140
+ args_count += 1
141
+ if arg.type == "spread_element":
142
+ has_vararg = True
143
+ elif arg.type == "object":
144
+ # If passing object literal, collect top-level property keys as kwargs
145
+ for obj_child in arg.children:
146
+ if obj_child.type in ("pair", "shorthand_property_identifier_pair"):
147
+ key_node = obj_child.child_by_field_name("key") or (
148
+ obj_child.children[0] if obj_child.children else None
149
+ )
150
+ if key_node:
151
+ kwargs.append(get_node_text(key_node, source_bytes))
152
+
153
+ if callee_name and callee_name != "require":
154
+ calls.append(
155
+ CallReference(
156
+ callee=callee_name,
157
+ args_count=args_count,
158
+ kwargs=kwargs,
159
+ lineno=n.start_point.row + 1,
160
+ caller=caller_id,
161
+ has_vararg=has_vararg,
162
+ )
163
+ )
164
+
165
+ for child in n.children:
166
+ walk(child)
167
+
168
+ walk(node)
169
+ return calls
170
+
171
+
172
+ def extract_typescript_imports(source: str, file_path: str = "") -> List[ImportReference]:
173
+ """Extract all import statements and require calls from TypeScript/JavaScript source."""
174
+ if not source.strip():
175
+ return []
176
+
177
+ parser = get_ts_parser(file_path)
178
+ source_bytes = source.encode("utf-8")
179
+ tree = parser.parse(source_bytes)
180
+
181
+ imports: List[ImportReference] = []
182
+
183
+ def walk(node: Node):
184
+ if node.type == "import_statement":
185
+ source_node = node.child_by_field_name("source")
186
+ mod_name = ""
187
+ if source_node:
188
+ mod_name = get_node_text(source_node, source_bytes).strip("'\"`")
189
+ lineno = node.start_point.row + 1
190
+
191
+ # Look for import clauses
192
+ clause = None
193
+ for ch in node.children:
194
+ if ch.type == "import_clause":
195
+ clause = ch
196
+ break
197
+
198
+ if not clause:
199
+ # e.g. import './styles.css'
200
+ imports.append(
201
+ ImportReference(
202
+ module=mod_name,
203
+ name="",
204
+ lineno=lineno,
205
+ file_path=file_path,
206
+ )
207
+ )
208
+ return
209
+
210
+ for ch in clause.children:
211
+ if ch.type == "identifier":
212
+ # Default import: import Foo from './mod'
213
+ imports.append(
214
+ ImportReference(
215
+ module=mod_name,
216
+ name=get_node_text(ch, source_bytes),
217
+ lineno=lineno,
218
+ file_path=file_path,
219
+ )
220
+ )
221
+ elif ch.type == "named_imports":
222
+ for spec in ch.children:
223
+ if spec.type == "import_specifier":
224
+ name_node = spec.child_by_field_name("name")
225
+ alias_node = spec.child_by_field_name("alias")
226
+ if name_node:
227
+ imp_name = get_node_text(name_node, source_bytes)
228
+ asname = get_node_text(alias_node, source_bytes) if alias_node else None
229
+ imports.append(
230
+ ImportReference(
231
+ module=mod_name,
232
+ name=imp_name,
233
+ asname=asname,
234
+ lineno=lineno,
235
+ file_path=file_path,
236
+ )
237
+ )
238
+ elif ch.type == "namespace_import":
239
+ # import * as Foo from './mod'
240
+ alias_node = None
241
+ for nch in ch.children:
242
+ if nch.type == "identifier":
243
+ alias_node = nch
244
+ alias_name = get_node_text(alias_node, source_bytes) if alias_node else "all"
245
+ imports.append(
246
+ ImportReference(
247
+ module=mod_name,
248
+ name="*",
249
+ asname=alias_name,
250
+ lineno=lineno,
251
+ file_path=file_path,
252
+ )
253
+ )
254
+
255
+ elif node.type == "export_statement" and node.child_by_field_name("source"):
256
+ source_node = node.child_by_field_name("source")
257
+ mod_name = get_node_text(source_node, source_bytes).strip("'\"`") if source_node else ""
258
+ lineno = node.start_point.row + 1
259
+ has_star = any(ch.type == "*" for ch in node.children)
260
+ if has_star:
261
+ imports.append(
262
+ ImportReference(
263
+ module=mod_name,
264
+ name="*",
265
+ lineno=lineno,
266
+ file_path=file_path,
267
+ )
268
+ )
269
+ else:
270
+ for ch in node.children:
271
+ if ch.type == "export_clause":
272
+ for spec in ch.children:
273
+ if spec.type == "export_specifier":
274
+ name_node = spec.child_by_field_name("name") or (
275
+ spec.children[0] if spec.children else None
276
+ )
277
+ alias_node = spec.child_by_field_name("alias")
278
+ if name_node:
279
+ imp_name = get_node_text(name_node, source_bytes)
280
+ asname = get_node_text(alias_node, source_bytes) if alias_node else None
281
+ imports.append(
282
+ ImportReference(
283
+ module=mod_name,
284
+ name=imp_name,
285
+ asname=asname,
286
+ lineno=lineno,
287
+ file_path=file_path,
288
+ )
289
+ )
290
+
291
+ elif node.type == "call_expression":
292
+ # const mod = require('./mod')
293
+ fn = node.child_by_field_name("function")
294
+ if fn and get_node_text(fn, source_bytes) == "require":
295
+ args = node.child_by_field_name("arguments")
296
+ if args and args.children:
297
+ for arg in args.children:
298
+ if arg.type == "string":
299
+ mod_name = get_node_text(arg, source_bytes).strip("'\"`")
300
+ # Determine identifier if assigned
301
+ parent = node.parent
302
+ asname = None
303
+ if parent and parent.type == "variable_declarator":
304
+ id_node = parent.child_by_field_name("name")
305
+ if id_node:
306
+ asname = get_node_text(id_node, source_bytes)
307
+ imports.append(
308
+ ImportReference(
309
+ module=mod_name,
310
+ name=asname or mod_name.split("/")[-1],
311
+ asname=asname,
312
+ lineno=node.start_point.row + 1,
313
+ file_path=file_path,
314
+ )
315
+ )
316
+
317
+ for child in node.children:
318
+ walk(child)
319
+
320
+ walk(tree.root_node)
321
+ return imports
322
+
323
+
324
+ def extract_typescript_symbols(source: str, file_path: str = "") -> List[Symbol]:
325
+ """Parse TypeScript/JavaScript source into AST and extract symbol entities."""
326
+ if not source.strip():
327
+ return []
328
+
329
+ parser = get_ts_parser(file_path)
330
+ source_bytes = source.encode("utf-8")
331
+ tree = parser.parse(source_bytes)
332
+
333
+ symbols: List[Symbol] = []
334
+
335
+ # Collect exported names from export clauses, default exports, and CommonJS
336
+ exported_names: Set[str] = set()
337
+ for top_child in tree.root_node.children:
338
+ if top_child.type in ("export_statement", "export_default_statement"):
339
+ for sc in top_child.children:
340
+ if sc.type == "export_clause":
341
+ for spec in sc.children:
342
+ if spec.type == "export_specifier":
343
+ name_node = spec.child_by_field_name("name") or (spec.children[0] if spec.children else None)
344
+ if name_node:
345
+ exported_names.add(get_node_text(name_node, source_bytes).strip())
346
+ elif sc.type == "identifier" and any(c.type == "default" for c in top_child.children):
347
+ exported_names.add(get_node_text(sc, source_bytes).strip())
348
+ elif top_child.type == "expression_statement":
349
+ for expr in top_child.children:
350
+ if expr.type == "assignment_expression":
351
+ left = expr.child_by_field_name("left")
352
+ right = expr.child_by_field_name("right")
353
+ if left and right:
354
+ left_text = get_node_text(left, source_bytes).strip()
355
+ if left_text == "module.exports" and right.type == "object":
356
+ for obj_child in right.children:
357
+ if obj_child.type in ("pair", "shorthand_property_identifier_pair"):
358
+ key_node = obj_child.child_by_field_name("key") or (obj_child.children[0] if obj_child.children else None)
359
+ if key_node:
360
+ exported_names.add(get_node_text(key_node, source_bytes).strip())
361
+ elif left_text.startswith("exports."):
362
+ prop = left_text.split(".", 1)[1].strip()
363
+ if prop:
364
+ exported_names.add(prop)
365
+
366
+ def process_node(node: Node, parent_qualname: Optional[str] = None):
367
+ # Unwrap export and ambient statements
368
+ target_node = node
369
+ is_exported = False
370
+ if node.type in ("export_statement", "export_default_statement"):
371
+ is_exported = True
372
+ for ch in node.children:
373
+ if ch.type in (
374
+ "function_declaration",
375
+ "class_declaration",
376
+ "interface_declaration",
377
+ "type_alias_declaration",
378
+ "enum_declaration",
379
+ "lexical_declaration",
380
+ "variable_declaration",
381
+ "internal_module",
382
+ "module",
383
+ ):
384
+ target_node = ch
385
+ break
386
+ elif node.type == "ambient_declaration":
387
+ for ch in node.children:
388
+ if ch.type in (
389
+ "function_declaration",
390
+ "class_declaration",
391
+ "interface_declaration",
392
+ "type_alias_declaration",
393
+ "enum_declaration",
394
+ "lexical_declaration",
395
+ "variable_declaration",
396
+ "internal_module",
397
+ "module",
398
+ ):
399
+ target_node = ch
400
+ break
401
+
402
+ if target_node.type == "expression_statement":
403
+ for ch in target_node.children:
404
+ if ch.type in ("internal_module", "module"):
405
+ target_node = ch
406
+ break
407
+
408
+ docstring = extract_preceding_docstring(node, source_bytes)
409
+ visibility = "public" if is_exported else "internal"
410
+
411
+ if target_node.type == "function_declaration":
412
+ name_node = target_node.child_by_field_name("name")
413
+ if not name_node:
414
+ if is_exported:
415
+ fn_name = "default"
416
+ else:
417
+ return
418
+ else:
419
+ fn_name = get_node_text(name_node, source_bytes)
420
+ qualname = f"{parent_qualname}.{fn_name}" if parent_qualname else fn_name
421
+ if not is_exported and (fn_name in exported_names or qualname in exported_names):
422
+ is_exported = True
423
+ visibility = "public"
424
+ sym_id = f"{file_path}::{qualname}"
425
+
426
+ is_async = any(ch.type == "async" for ch in target_node.children)
427
+ params_node = target_node.child_by_field_name("parameters")
428
+ params = _extract_parameters(params_node, source_bytes) if params_node else []
429
+
430
+ ret_node = target_node.child_by_field_name("return_type")
431
+ ret_type = get_node_text(ret_node, source_bytes).lstrip(": ").strip() if ret_node else None
432
+
433
+ # Calculate arity
434
+ pos_params = [p for p in params if not p.is_vararg]
435
+ min_args = len([p for p in pos_params if not p.has_default])
436
+ max_args = None if any(p.is_vararg for p in params) else len(pos_params)
437
+
438
+ # Calls inside body
439
+ body_node = target_node.child_by_field_name("body")
440
+ calls = _extract_calls(body_node, source_bytes, caller_id=sym_id) if body_node else []
441
+
442
+ prefix = "async function" if is_async else "function"
443
+ param_strs = [p.name + (f": {p.annotation}" if p.annotation else "") for p in params]
444
+ ret_suffix = f": {ret_type}" if ret_type else ""
445
+ signature = f"{prefix} {fn_name}({', '.join(param_strs)}){ret_suffix}"
446
+
447
+ symbol = Symbol(
448
+ name=fn_name,
449
+ qualname=qualname,
450
+ file_path=file_path,
451
+ kind="async_function" if is_async else "function",
452
+ lineno=target_node.start_point.row + 1,
453
+ end_lineno=target_node.end_point.row + 1,
454
+ signature=signature,
455
+ params=params,
456
+ min_args=min_args,
457
+ max_args=max_args,
458
+ return_type=ret_type,
459
+ calls=calls,
460
+ is_method=False,
461
+ is_static=False,
462
+ docstring=docstring,
463
+ is_exported=is_exported,
464
+ visibility=visibility,
465
+ )
466
+ symbols.append(symbol)
467
+
468
+ elif target_node.type in ("lexical_declaration", "variable_declaration"):
469
+ # Handle: const fn = (a, b) => ... or let fn = function(...) ...
470
+ is_const = any(ch.type == "const" for ch in target_node.children)
471
+ for decl in target_node.children:
472
+ if decl.type == "variable_declarator":
473
+ name_node = decl.child_by_field_name("name")
474
+ val_node = decl.child_by_field_name("value")
475
+ if name_node and val_node and val_node.type in ("arrow_function", "function_expression"):
476
+ fn_name = get_node_text(name_node, source_bytes)
477
+ qualname = f"{parent_qualname}.{fn_name}" if parent_qualname else fn_name
478
+ sym_id = f"{file_path}::{qualname}"
479
+
480
+ is_async = any(ch.type == "async" for ch in val_node.children)
481
+ params_node = val_node.child_by_field_name("parameters")
482
+ params = _extract_parameters(params_node, source_bytes) if params_node else []
483
+ if not params_node:
484
+ # Single param arrow function e.g. x => x * 2
485
+ param_id = val_node.child_by_field_name("parameter")
486
+ if param_id:
487
+ params = [Parameter(name=get_node_text(param_id, source_bytes))]
488
+
489
+ ret_node = val_node.child_by_field_name("return_type")
490
+ ret_type = get_node_text(ret_node, source_bytes).lstrip(": ").strip() if ret_node else None
491
+
492
+ pos_params = [p for p in params if not p.is_vararg]
493
+ min_args = len([p for p in pos_params if not p.has_default])
494
+ max_args = None if any(p.is_vararg for p in params) else len(pos_params)
495
+
496
+ body_node = val_node.child_by_field_name("body")
497
+ calls = _extract_calls(body_node, source_bytes, caller_id=sym_id) if body_node else []
498
+
499
+ param_strs = [p.name + (f": {p.annotation}" if p.annotation else "") for p in params]
500
+ signature = f"const {fn_name} = ({', '.join(param_strs)}) => ..."
501
+
502
+ symbol = Symbol(
503
+ name=fn_name,
504
+ qualname=qualname,
505
+ file_path=file_path,
506
+ kind="async_function" if is_async else "function",
507
+ lineno=target_node.start_point.row + 1,
508
+ end_lineno=target_node.end_point.row + 1,
509
+ signature=signature,
510
+ params=params,
511
+ min_args=min_args,
512
+ max_args=max_args,
513
+ return_type=ret_type,
514
+ calls=calls,
515
+ is_method=False,
516
+ is_static=False,
517
+ docstring=docstring,
518
+ is_exported=is_exported,
519
+ visibility=visibility,
520
+ )
521
+ symbols.append(symbol)
522
+ elif name_node:
523
+ # Top-level constant or variable
524
+ var_name = get_node_text(name_node, source_bytes)
525
+ if var_name and var_name.isidentifier():
526
+ qualname = f"{parent_qualname}.{var_name}" if parent_qualname else var_name
527
+ kind = "constant" if is_const else "variable"
528
+ sig_prefix = "const" if is_const else "let"
529
+ symbols.append(
530
+ Symbol(
531
+ name=var_name,
532
+ qualname=qualname,
533
+ file_path=file_path,
534
+ kind=kind,
535
+ lineno=target_node.start_point.row + 1,
536
+ end_lineno=target_node.end_point.row + 1,
537
+ signature=f"{sig_prefix} {var_name}",
538
+ min_args=0,
539
+ max_args=0,
540
+ docstring=docstring,
541
+ is_exported=is_exported,
542
+ visibility=visibility,
543
+ )
544
+ )
545
+
546
+ elif target_node.type == "class_declaration":
547
+ name_node = target_node.child_by_field_name("name")
548
+ if not name_node:
549
+ if is_exported:
550
+ class_name = "default"
551
+ else:
552
+ return
553
+ else:
554
+ class_name = get_node_text(name_node, source_bytes)
555
+ qualname = f"{parent_qualname}.{class_name}" if parent_qualname else class_name
556
+ if not is_exported and (class_name in exported_names or qualname in exported_names):
557
+ is_exported = True
558
+ visibility = "public"
559
+ sym_id = f"{file_path}::{qualname}"
560
+
561
+ # Base classes & interfaces
562
+ bases: List[str] = []
563
+ heritage_node = None
564
+ for ch in target_node.children:
565
+ if ch.type == "class_heritage":
566
+ heritage_node = ch
567
+ break
568
+
569
+ if heritage_node:
570
+ for h_child in heritage_node.children:
571
+ if h_child.type in ("extends_clause", "implements_clause"):
572
+ for base_ch in h_child.children:
573
+ if base_ch.type in ("identifier", "type_identifier"):
574
+ bases.append(get_node_text(base_ch, source_bytes))
575
+
576
+ class_body = target_node.child_by_field_name("body")
577
+ class_calls = _extract_calls(class_body, source_bytes, caller_id=sym_id) if class_body else []
578
+
579
+ bases_str = f" extends {', '.join(bases)}" if bases else ""
580
+ signature = f"class {class_name}{bases_str}"
581
+
582
+ class_sym = Symbol(
583
+ name=class_name,
584
+ qualname=qualname,
585
+ file_path=file_path,
586
+ kind="class",
587
+ lineno=target_node.start_point.row + 1,
588
+ end_lineno=target_node.end_point.row + 1,
589
+ signature=signature,
590
+ calls=class_calls,
591
+ bases=bases,
592
+ docstring=docstring,
593
+ is_exported=is_exported,
594
+ visibility=visibility,
595
+ )
596
+ symbols.append(class_sym)
597
+
598
+ # Process methods inside class_body
599
+ if class_body:
600
+ for member in class_body.children:
601
+ if member.type == "method_definition":
602
+ m_name_node = member.child_by_field_name("name")
603
+ if not m_name_node:
604
+ continue
605
+ m_name = get_node_text(m_name_node, source_bytes)
606
+ m_qualname = f"{qualname}.{m_name}"
607
+ m_sym_id = f"{file_path}::{m_qualname}"
608
+
609
+ is_static = any(ch.type == "static" for ch in member.children)
610
+ is_async = any(ch.type == "async" for ch in member.children)
611
+ params_node = member.child_by_field_name("parameters")
612
+ raw_params = _extract_parameters(params_node, source_bytes) if params_node else []
613
+
614
+ # Prepend receiver 'this' if non-static method
615
+ if not is_static:
616
+ params = [Parameter(name="this", annotation=class_name)] + raw_params
617
+ is_method = True
618
+ else:
619
+ params = raw_params
620
+ is_method = False
621
+
622
+ ret_node = member.child_by_field_name("return_type")
623
+ ret_type = get_node_text(ret_node, source_bytes).lstrip(": ").strip() if ret_node else None
624
+
625
+ pos_params = [p for p in raw_params if not p.is_vararg]
626
+ min_args = len([p for p in pos_params if not p.has_default])
627
+ max_args = None if any(p.is_vararg for p in raw_params) else len(pos_params)
628
+
629
+ body_node = member.child_by_field_name("body")
630
+ calls = _extract_calls(body_node, source_bytes, caller_id=m_sym_id) if body_node else []
631
+
632
+ m_prefix = ("static " if is_static else "") + ("async " if is_async else "")
633
+ param_strs = [p.name + (f": {p.annotation}" if p.annotation else "") for p in raw_params]
634
+ ret_suffix = f": {ret_type}" if ret_type else ""
635
+ m_sig = f"{m_prefix}{m_name}({', '.join(param_strs)}){ret_suffix}"
636
+
637
+ m_doc = extract_preceding_docstring(member, source_bytes)
638
+ m_is_priv = any(ch.type == "accessibility_modifier" and get_node_text(ch, source_bytes) == "private" for ch in member.children) or m_name.startswith("#")
639
+ m_is_prot = any(ch.type == "accessibility_modifier" and get_node_text(ch, source_bytes) == "protected" for ch in member.children)
640
+ if m_is_priv:
641
+ m_vis = "private"
642
+ m_exp = False
643
+ elif m_is_prot:
644
+ m_vis = "internal"
645
+ m_exp = False
646
+ else:
647
+ m_vis = "public" if is_exported else "internal"
648
+ m_exp = is_exported
649
+
650
+ method_sym = Symbol(
651
+ name=m_name,
652
+ qualname=m_qualname,
653
+ file_path=file_path,
654
+ kind="method" if not is_static else "function",
655
+ lineno=member.start_point.row + 1,
656
+ end_lineno=member.end_point.row + 1,
657
+ signature=m_sig,
658
+ params=params,
659
+ min_args=min_args,
660
+ max_args=max_args,
661
+ return_type=ret_type,
662
+ calls=calls,
663
+ is_method=is_method,
664
+ is_static=is_static,
665
+ docstring=m_doc,
666
+ is_exported=m_exp,
667
+ visibility=m_vis,
668
+ )
669
+ symbols.append(method_sym)
670
+
671
+ elif target_node.type == "interface_declaration":
672
+ name_node = target_node.child_by_field_name("name")
673
+ if name_node:
674
+ if_name = get_node_text(name_node, source_bytes)
675
+ qualname = f"{parent_qualname}.{if_name}" if parent_qualname else if_name
676
+ symbols.append(
677
+ Symbol(
678
+ name=if_name,
679
+ qualname=qualname,
680
+ file_path=file_path,
681
+ kind="interface",
682
+ lineno=target_node.start_point.row + 1,
683
+ end_lineno=target_node.end_point.row + 1,
684
+ signature=f"interface {if_name}",
685
+ docstring=docstring,
686
+ is_exported=is_exported,
687
+ visibility=visibility,
688
+ )
689
+ )
690
+
691
+ elif target_node.type == "type_alias_declaration":
692
+ name_node = target_node.child_by_field_name("name")
693
+ if name_node:
694
+ t_name = get_node_text(name_node, source_bytes)
695
+ qualname = f"{parent_qualname}.{t_name}" if parent_qualname else t_name
696
+ symbols.append(
697
+ Symbol(
698
+ name=t_name,
699
+ qualname=qualname,
700
+ file_path=file_path,
701
+ kind="type_alias",
702
+ lineno=target_node.start_point.row + 1,
703
+ end_lineno=target_node.end_point.row + 1,
704
+ signature=f"type {t_name}",
705
+ docstring=docstring,
706
+ is_exported=is_exported,
707
+ visibility=visibility,
708
+ )
709
+ )
710
+
711
+ elif target_node.type == "enum_declaration":
712
+ name_node = target_node.child_by_field_name("name")
713
+ if name_node:
714
+ enum_name = get_node_text(name_node, source_bytes)
715
+ qualname = f"{parent_qualname}.{enum_name}" if parent_qualname else enum_name
716
+ symbols.append(
717
+ Symbol(
718
+ name=enum_name,
719
+ qualname=qualname,
720
+ file_path=file_path,
721
+ kind="enum",
722
+ lineno=target_node.start_point.row + 1,
723
+ end_lineno=target_node.end_point.row + 1,
724
+ signature=f"enum {enum_name}",
725
+ min_args=0,
726
+ max_args=0,
727
+ docstring=docstring,
728
+ is_exported=is_exported,
729
+ visibility=visibility,
730
+ )
731
+ )
732
+
733
+ elif target_node.type in ("internal_module", "module"):
734
+ name_node = target_node.child_by_field_name("name")
735
+ ns_name = get_node_text(name_node, source_bytes).strip("'\"`") if name_node else None
736
+ ns_qualname = f"{parent_qualname}.{ns_name}" if parent_qualname and ns_name else ns_name
737
+ body_node = target_node.child_by_field_name("body") or next(
738
+ (c for c in target_node.children if c.type == "statement_block"), None
739
+ )
740
+ if body_node:
741
+ for b_child in body_node.children:
742
+ process_node(b_child, parent_qualname=ns_qualname)
743
+
744
+ for child in tree.root_node.children:
745
+ process_node(child)
746
+
747
+ # Post-process symbols that were exported via separate export clauses
748
+ for s in symbols:
749
+ if s.name in exported_names or s.qualname in exported_names:
750
+ s.is_exported = True
751
+ s.visibility = "public"
752
+
753
+ # Extract module-level calls
754
+ all_calls = _extract_calls(tree.root_node, source_bytes, caller_id=f"{file_path}::<module>")
755
+ module_calls = [
756
+ c for c in all_calls
757
+ if not any(s.lineno <= c.lineno <= s.end_lineno for s in symbols if s.kind != "module")
758
+ ]
759
+ if module_calls:
760
+ line_count = len(source.splitlines()) or 1
761
+ module_sym = Symbol(
762
+ name="<module>",
763
+ qualname="<module>",
764
+ file_path=file_path,
765
+ kind="module",
766
+ lineno=1,
767
+ end_lineno=line_count,
768
+ signature=f"// module {file_path}",
769
+ calls=module_calls,
770
+ is_exported=True,
771
+ visibility="public",
772
+ )
773
+ symbols.append(module_sym)
774
+
775
+ return symbols