1mod calls;
10mod imports;
11mod type_defs;
12mod types;
13
14pub use types::*;
15
16#[cfg(feature = "tree-sitter")]
17use tree_sitter::{Language, Node, Parser};
18
19pub fn analyze(content: &str, ext: &str) -> DeepAnalysis {
20 #[cfg(feature = "tree-sitter")]
21 {
22 if let Some(result) = analyze_with_tree_sitter(content, ext) {
23 return result;
24 }
25 }
26
27 let _ = (content, ext);
28 DeepAnalysis::empty()
29}
30
31#[cfg(feature = "tree-sitter")]
32fn analyze_with_tree_sitter(content: &str, ext: &str) -> Option<DeepAnalysis> {
33 let language = get_language(ext)?;
34
35 thread_local! {
36 static PARSER: std::cell::RefCell<Parser> = std::cell::RefCell::new(Parser::new());
37 }
38
39 let tree = PARSER.with(|p| {
40 let mut parser = p.borrow_mut();
41 let _ = parser.set_language(&language);
42 parser.parse(content.as_bytes(), None)
43 })?;
44 let root = tree.root_node();
45
46 let imports = imports::extract_imports(root, content, ext);
47 let calls = calls::extract_calls(root, content, ext);
48 let types = type_defs::extract_types(root, content, ext);
49 let exports = type_defs::extract_exports(root, content, ext);
50
51 Some(DeepAnalysis {
52 imports,
53 calls,
54 types,
55 exports,
56 })
57}
58
59#[cfg(feature = "tree-sitter")]
60fn get_language(ext: &str) -> Option<Language> {
61 match ext {
62 "rs" => Some(tree_sitter_rust::LANGUAGE.into()),
63 "ts" | "tsx" => Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
64 "js" | "jsx" => Some(tree_sitter_javascript::LANGUAGE.into()),
65 "py" => Some(tree_sitter_python::LANGUAGE.into()),
66 "go" => Some(tree_sitter_go::LANGUAGE.into()),
67 "java" => Some(tree_sitter_java::LANGUAGE.into()),
68 "c" | "h" => Some(tree_sitter_c::LANGUAGE.into()),
69 "cpp" | "cc" | "cxx" | "hpp" | "hxx" | "hh" => Some(tree_sitter_cpp::LANGUAGE.into()),
70 "rb" => Some(tree_sitter_ruby::LANGUAGE.into()),
71 "cs" => Some(tree_sitter_c_sharp::LANGUAGE.into()),
72 "kt" | "kts" => Some(tree_sitter_kotlin_ng::LANGUAGE.into()),
73 "swift" => Some(tree_sitter_swift::LANGUAGE.into()),
74 "php" => Some(tree_sitter_php::LANGUAGE_PHP.into()),
75 "sh" | "bash" => Some(tree_sitter_bash::LANGUAGE.into()),
76 "dart" => Some(tree_sitter_dart::LANGUAGE.into()),
77 "scala" | "sc" => Some(tree_sitter_scala::LANGUAGE.into()),
78 "ex" | "exs" => Some(tree_sitter_elixir::LANGUAGE.into()),
79 "zig" => Some(tree_sitter_zig::LANGUAGE.into()),
80 "gd" => Some(tree_sitter_gdscript::LANGUAGE.into()),
81 _ => None,
82 }
83}
84
85#[cfg(feature = "tree-sitter")]
90fn node_text<'a>(node: Node, src: &'a str) -> &'a str {
91 &src[node.byte_range()]
92}
93
94#[cfg(feature = "tree-sitter")]
95fn find_child_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
96 let mut cursor = node.walk();
97 let result = node.children(&mut cursor).find(|c| c.kind() == kind);
98 result
99}
100
101#[cfg(feature = "tree-sitter")]
102fn find_descendant_by_kind<'a>(node: Node<'a>, kind: &str) -> Option<Node<'a>> {
103 crate::core::ast_walk::find_descendant_by_kind(node, kind)
105}
106
107#[cfg(test)]
112#[cfg(feature = "tree-sitter")]
113mod tests {
114 use super::*;
115
116 #[test]
122 fn deeply_nested_source_does_not_overflow() {
123 let depth = 12_000;
124 let rs = format!(
128 "fn m() {{ let _ = {}0{}; }}",
129 "f(".repeat(depth),
130 ")".repeat(depth)
131 );
132 let analysis = analyze(&rs, "rs");
133 assert!(!analysis.calls.is_empty());
134 }
135
136 #[test]
137 fn ts_named_import() {
138 let src = r"import { useState, useEffect } from 'react';";
139 let analysis = analyze(src, "ts");
140 assert_eq!(analysis.imports.len(), 1);
141 assert_eq!(analysis.imports[0].source, "react");
142 assert_eq!(analysis.imports[0].names, vec!["useState", "useEffect"]);
143 }
144
145 #[test]
146 fn ts_default_import() {
147 let src = r"import React from 'react';";
148 let analysis = analyze(src, "ts");
149 assert_eq!(analysis.imports.len(), 1);
150 assert_eq!(analysis.imports[0].kind, ImportKind::Default);
151 assert_eq!(analysis.imports[0].names, vec!["React"]);
152 }
153
154 #[test]
155 fn ts_star_import() {
156 let src = r"import * as path from 'path';";
157 let analysis = analyze(src, "ts");
158 assert_eq!(analysis.imports.len(), 1);
159 assert_eq!(analysis.imports[0].kind, ImportKind::Star);
160 }
161
162 #[test]
163 fn ts_side_effect_import() {
164 let src = r"import './styles.css';";
165 let analysis = analyze(src, "ts");
166 assert_eq!(analysis.imports.len(), 1);
167 assert_eq!(analysis.imports[0].kind, ImportKind::SideEffect);
168 assert_eq!(analysis.imports[0].source, "./styles.css");
169 }
170
171 #[test]
172 fn ts_type_only_import() {
173 let src = r"import type { User } from './types';";
174 let analysis = analyze(src, "ts");
175 assert_eq!(analysis.imports.len(), 1);
176 assert!(analysis.imports[0].is_type_only);
177 }
178
179 #[test]
180 fn ts_reexport() {
181 let src = r"export { foo, bar } from './utils';";
182 let analysis = analyze(src, "ts");
183 assert_eq!(analysis.imports.len(), 1);
184 assert_eq!(analysis.imports[0].kind, ImportKind::Reexport);
185 }
186
187 #[test]
188 fn ts_call_sites() {
189 let src = r"
190const x = foo(1);
191const y = obj.method(2);
192";
193 let analysis = analyze(src, "ts");
194 assert!(analysis.calls.len() >= 2);
195 let fns: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
196 assert!(fns.contains(&"foo"));
197 assert!(fns.contains(&"method"));
198 }
199
200 #[test]
201 fn ts_interface() {
202 let src = r"
203export interface User {
204 name: string;
205 age: number;
206}
207";
208 let analysis = analyze(src, "ts");
209 assert_eq!(analysis.types.len(), 1);
210 assert_eq!(analysis.types[0].name, "User");
211 assert_eq!(analysis.types[0].kind, TypeDefKind::Interface);
212 }
213
214 #[test]
215 fn ts_type_alias_union() {
216 let src = r"type Result = Success | Error;";
217 let analysis = analyze(src, "ts");
218 assert_eq!(analysis.types.len(), 1);
219 assert_eq!(analysis.types[0].kind, TypeDefKind::Union);
220 }
221
222 #[test]
223 fn rust_use_statements() {
224 let src = r"
225use crate::core::session;
226use anyhow::Result;
227use std::collections::HashMap;
228";
229 let analysis = analyze(src, "rs");
230 assert_eq!(analysis.imports.len(), 2);
231 let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
232 assert!(sources.contains(&"crate::core::session"));
233 assert!(sources.contains(&"anyhow::Result"));
234 }
235
236 #[test]
237 fn rust_pub_use_reexport() {
238 let src = r"pub use crate::tools::ctx_read;";
239 let analysis = analyze(src, "rs");
240 assert_eq!(analysis.imports.len(), 1);
241 assert_eq!(analysis.imports[0].kind, ImportKind::Reexport);
242 }
243
244 #[test]
245 fn rust_struct_and_trait() {
246 let src = r"
247pub struct Config {
248 pub name: String,
249}
250
251pub trait Service {
252 fn run(&self);
253}
254";
255 let analysis = analyze(src, "rs");
256 assert_eq!(analysis.types.len(), 2);
257 let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
258 assert!(names.contains(&"Config"));
259 assert!(names.contains(&"Service"));
260 }
261
262 #[test]
263 fn rust_call_sites() {
264 let src = r"
265fn main() {
266 let x = calculate(42);
267 let y = self.process();
268 Vec::new();
269}
270";
271 let analysis = analyze(src, "rs");
272 assert!(analysis.calls.len() >= 2);
273 let fns: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
274 assert!(fns.contains(&"calculate"));
275 }
276
277 #[test]
278 fn python_imports() {
279 let src = r"
280import os
281from pathlib import Path
282from . import utils
283from ..models import User, Role
284";
285 let analysis = analyze(src, "py");
286 assert!(analysis.imports.len() >= 3);
287 }
288
289 #[test]
290 fn python_class_protocol() {
291 let src = r"
292class MyProtocol(Protocol):
293 def method(self) -> None: ...
294
295class User:
296 name: str
297";
298 let analysis = analyze(src, "py");
299 assert_eq!(analysis.types.len(), 2);
300 assert_eq!(analysis.types[0].kind, TypeDefKind::Protocol);
301 assert_eq!(analysis.types[1].kind, TypeDefKind::Class);
302 }
303
304 #[test]
305 fn python_call_sites() {
306 let src = r"
309from models.engine import Engine
310
311def boot():
312 engine = Engine(power=100)
313 engine.run()
314 return engine
315";
316 let analysis = analyze(src, "py");
317 let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
318 assert!(
319 callees.contains(&"Engine"),
320 "class instantiation should be a call site, got {callees:?}"
321 );
322 assert!(
323 callees.contains(&"run"),
324 "method call must resolve to the method name (not the receiver), got {callees:?}"
325 );
326 }
327
328 #[test]
329 fn java_object_creation_is_a_call_site() {
330 let src = r"
331class App {
332 void boot() {
333 Engine e = new Engine(100);
334 e.run();
335 }
336}
337";
338 let analysis = analyze(src, "java");
339 let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
340 assert!(
341 callees.contains(&"Engine"),
342 "`new Engine()` should be a call site, got {callees:?}"
343 );
344 assert!(
345 callees.contains(&"run"),
346 "method call expected, got {callees:?}"
347 );
348 }
349
350 #[test]
351 fn go_imports() {
352 let src = r#"
353package main
354
355import (
356 "fmt"
357 "net/http"
358 _ "github.com/lib/pq"
359)
360"#;
361 let analysis = analyze(src, "go");
362 assert!(analysis.imports.len() >= 3);
363 let side_effect = analysis.imports.iter().find(|i| i.source.contains("pq"));
364 assert!(side_effect.is_some());
365 assert_eq!(side_effect.unwrap().kind, ImportKind::SideEffect);
366 }
367
368 #[test]
369 fn go_struct_and_interface() {
370 let src = r"
371package main
372
373type Server struct {
374 Port int
375}
376
377type Handler interface {
378 Handle(r *Request)
379}
380";
381 let analysis = analyze(src, "go");
382 assert_eq!(analysis.types.len(), 2);
383 let kinds: Vec<&TypeDefKind> = analysis.types.iter().map(|t| &t.kind).collect();
384 assert!(kinds.contains(&&TypeDefKind::Struct));
385 assert!(kinds.contains(&&TypeDefKind::Interface));
386 }
387
388 #[test]
389 fn java_imports() {
390 let src = r"
391import java.util.List;
392import java.util.Map;
393import static org.junit.Assert.*;
394";
395 let analysis = analyze(src, "java");
396 assert!(analysis.imports.len() >= 2);
397 }
398
399 #[test]
400 fn java_class_and_interface() {
401 let src = r"
402public class UserService {
403 public void save(User u) {}
404}
405
406public interface Repository<T> {
407 T findById(int id);
408}
409
410public enum Status { ACTIVE, INACTIVE }
411
412public record Point(int x, int y) {}
413";
414 let analysis = analyze(src, "java");
415 assert!(analysis.types.len() >= 3);
416 let kinds: Vec<&TypeDefKind> = analysis.types.iter().map(|t| &t.kind).collect();
417 assert!(kinds.contains(&&TypeDefKind::Class));
418 assert!(kinds.contains(&&TypeDefKind::Interface));
419 assert!(kinds.contains(&&TypeDefKind::Enum));
420 }
421
422 #[test]
423 fn kotlin_imports_and_aliases() {
424 let src = r"
425package com.example.app
426
427import com.example.services.UserService
428import com.example.factories.WidgetFactory as Factory
429import com.example.shared.*
430";
431 let analysis = analyze(src, "kt");
432 assert_eq!(analysis.imports.len(), 3);
433 assert_eq!(
434 analysis.imports[0].source,
435 "com.example.services.UserService"
436 );
437 assert_eq!(analysis.imports[1].names, vec!["Factory"]);
438 assert_eq!(analysis.imports[2].kind, ImportKind::Star);
439 }
440
441 #[test]
442 fn kotlin_call_sites() {
443 let src = r"
444class UserService {
445 fun run() {
446 prepare()
447 repository.save(user)
448 Factory.create()
449 }
450}
451";
452 let analysis = analyze(src, "kt");
453 let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
454 assert!(callees.contains(&"prepare"));
455 assert!(callees.contains(&"save"));
456 assert!(callees.contains(&"create"));
457 }
458
459 #[test]
460 fn kotlin_types_and_visibility() {
461 let src = r"
462sealed interface Handler
463data class User(val id: String)
464enum class Status { ACTIVE, INACTIVE }
465object Registry
466private typealias UserId = String
467";
468 let analysis = analyze(src, "kt");
469 let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
470 assert!(names.contains(&"Handler"));
471 assert!(names.contains(&"User"));
472 assert!(names.contains(&"Status"));
473 assert!(names.contains(&"Registry"));
474 assert!(names.contains(&"UserId"));
475 let handler = analysis.types.iter().find(|t| t.name == "Handler").unwrap();
476 assert_eq!(handler.kind, TypeDefKind::Interface);
477 let alias = analysis.types.iter().find(|t| t.name == "UserId").unwrap();
478 assert!(!alias.is_exported);
479 }
480
481 #[test]
482 fn ts_generics_extracted() {
483 let src = r"interface Result<T, E> { ok: T; err: E; }";
484 let analysis = analyze(src, "ts");
485 assert_eq!(analysis.types.len(), 1);
486 assert!(!analysis.types[0].generics.is_empty());
487 }
488
489 #[test]
490 fn mixed_analysis_ts() {
491 let src = r"
492import { Request, Response } from 'express';
493import type { User } from './models';
494
495export interface Handler {
496 handle(req: Request): Response;
497}
498
499export class Router {
500 register(path: string, handler: Handler) {
501 this.handlers.set(path, handler);
502 }
503}
504
505const app = express();
506app.listen(3000);
507";
508 let analysis = analyze(src, "ts");
509 assert!(analysis.imports.len() >= 2, "Should find imports");
510 assert!(!analysis.types.is_empty(), "Should find types");
511 assert!(!analysis.calls.is_empty(), "Should find calls");
512 }
513
514 #[test]
515 fn empty_file() {
516 let analysis = analyze("", "ts");
517 assert!(analysis.imports.is_empty());
518 assert!(analysis.calls.is_empty());
519 assert!(analysis.types.is_empty());
520 }
521
522 #[test]
523 fn unsupported_extension() {
524 let analysis = analyze("some content", "txt");
525 assert!(analysis.imports.is_empty());
526 }
527
528 #[test]
529 fn c_include_import() {
530 let src = r#"
531#include "foo/bar.h"
532#include <stdio.h>
533"#;
534 let analysis = analyze(src, "c");
535 assert!(analysis.imports.iter().any(|i| i.source == "foo/bar.h"));
536 }
537
538 #[test]
539 fn bash_source_import() {
540 let src = r#"
541source "./scripts/env.sh"
542. ../common.sh
543"#;
544 let analysis = analyze(src, "sh");
545 assert!(
546 analysis
547 .imports
548 .iter()
549 .any(|i| i.source.contains("scripts/env.sh")),
550 "expected source import"
551 );
552 }
553
554 #[test]
555 fn zig_at_import() {
556 let src = r#"
557const m = @import("lib/math.zig");
558const std = @import("std");
559"#;
560 let analysis = analyze(src, "zig");
561 assert!(analysis.imports.iter().any(|i| i.source == "lib/math.zig"));
562 }
563
564 #[test]
565 fn gdscript_imports_extends_and_preload() {
566 let src = r#"
567extends "res://actors/base_actor.gd"
568
569const Bullet = preload("res://weapons/bullet.gd")
570var sfx = load("res://audio/shot.wav")
571"#;
572 let analysis = analyze(src, "gd");
573 let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
574 assert!(
575 sources.contains(&"res://actors/base_actor.gd"),
576 "expected extends import, got {sources:?}"
577 );
578 assert!(
579 sources.contains(&"res://weapons/bullet.gd"),
580 "expected preload import, got {sources:?}"
581 );
582 assert!(
583 sources.contains(&"res://audio/shot.wav"),
584 "expected load import, got {sources:?}"
585 );
586 }
587
588 #[test]
589 fn gdscript_types_class_name_and_enum() {
590 let src = r"
591class_name Player
592
593enum State { IDLE, RUNNING }
594
595class Inventory:
596 var items = []
597";
598 let analysis = analyze(src, "gd");
599 let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
600 assert!(
601 names.contains(&"Player"),
602 "expected class_name, got {names:?}"
603 );
604 assert!(names.contains(&"State"), "expected enum, got {names:?}");
605 assert!(
606 names.contains(&"Inventory"),
607 "expected inner class, got {names:?}"
608 );
609 let player = analysis.types.iter().find(|t| t.name == "Player").unwrap();
610 assert_eq!(player.kind, TypeDefKind::Class);
611 assert!(player.is_exported);
612 let state = analysis.types.iter().find(|t| t.name == "State").unwrap();
613 assert_eq!(state.kind, TypeDefKind::Enum);
614 }
615
616 #[test]
617 fn csharp_imports_all_using_forms() {
618 let src = r"
619using System;
620using System.Collections.Generic;
621global using MyApp.Core;
622using static System.Math;
623using Json = Newtonsoft.Json;
624namespace MyApp.Services {
625 using MyApp.Data.Repositories;
626}
627";
628 let analysis = analyze(src, "cs");
629 let sources: Vec<&str> = analysis.imports.iter().map(|i| i.source.as_str()).collect();
630 assert!(sources.contains(&"System"), "plain using, got {sources:?}");
631 assert!(
632 sources.contains(&"System.Collections.Generic"),
633 "dotted using, got {sources:?}"
634 );
635 assert!(
636 sources.contains(&"MyApp.Core"),
637 "global using must drop the `global` keyword, got {sources:?}"
638 );
639 assert!(
640 sources.contains(&"System.Math"),
641 "using static must drop the `static` keyword, got {sources:?}"
642 );
643 assert!(
644 sources.contains(&"Newtonsoft.Json"),
645 "alias using must keep the right-hand namespace, got {sources:?}"
646 );
647 assert!(
648 sources.contains(&"MyApp.Data.Repositories"),
649 "using nested inside a namespace block must be found, got {sources:?}"
650 );
651 }
652
653 #[test]
654 fn csharp_types_and_visibility() {
655 let src = r"
656namespace App
657{
658 public class UserService { }
659 internal class Helper { }
660 public interface IRepository { }
661 public struct Point { public int X; }
662 public enum Status { Active, Inactive }
663 public record Money(decimal Amount, string Currency);
664}
665";
666 let analysis = analyze(src, "cs");
667 let names: Vec<&str> = analysis.types.iter().map(|t| t.name.as_str()).collect();
668 assert!(names.contains(&"UserService"), "class, got {names:?}");
669 assert!(names.contains(&"Helper"), "internal class, got {names:?}");
670 assert!(names.contains(&"IRepository"), "interface, got {names:?}");
671 assert!(names.contains(&"Point"), "struct, got {names:?}");
672 assert!(names.contains(&"Status"), "enum, got {names:?}");
673 assert!(names.contains(&"Money"), "record, got {names:?}");
674
675 let kind_of = |n: &str| {
676 analysis
677 .types
678 .iter()
679 .find(|t| t.name == n)
680 .map(|t| t.kind.clone())
681 };
682 assert_eq!(kind_of("UserService"), Some(TypeDefKind::Class));
683 assert_eq!(kind_of("IRepository"), Some(TypeDefKind::Interface));
684 assert_eq!(kind_of("Point"), Some(TypeDefKind::Struct));
685 assert_eq!(kind_of("Status"), Some(TypeDefKind::Enum));
686 assert_eq!(kind_of("Money"), Some(TypeDefKind::Record));
687
688 let exported = |n: &str| {
689 analysis
690 .types
691 .iter()
692 .find(|t| t.name == n)
693 .is_some_and(|t| t.is_exported)
694 };
695 assert!(exported("UserService"), "public class is exported");
696 assert!(
697 !exported("Helper"),
698 "internal class must not be marked exported"
699 );
700 assert!(analysis.exports.contains(&"UserService".to_string()));
701 }
702
703 #[test]
704 fn csharp_call_sites() {
705 let src = r"
706namespace App
707{
708 public class Boot
709 {
710 public void Run()
711 {
712 Prepare();
713 _repository.Save(user);
714 var engine = new Engine(100);
715 Factory.Create<Widget>();
716 }
717 }
718}
719";
720 let analysis = analyze(src, "cs");
721 let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
722 assert!(
723 callees.contains(&"Prepare"),
724 "direct invocation, got {callees:?}"
725 );
726 assert!(
727 callees.contains(&"Save"),
728 "member invocation must resolve to the method name, got {callees:?}"
729 );
730 assert!(
731 callees.contains(&"Engine"),
732 "`new Engine()` should reference the constructed type, got {callees:?}"
733 );
734 assert!(
735 callees.contains(&"Create"),
736 "generic member call must reduce to the identifier, got {callees:?}"
737 );
738
739 let save = analysis.calls.iter().find(|c| c.callee == "Save").unwrap();
740 assert_eq!(save.receiver.as_deref(), Some("_repository"));
741 assert!(save.is_method);
742 }
743
744 #[test]
745 fn gdscript_calls_method_and_instantiation() {
746 let src = r"
747func _ready():
748 var mgr = MapDataManager.new()
749 mgr.load_map_data()
750 update_state()
751";
752 let analysis = analyze(src, "gd");
753 let callees: Vec<&str> = analysis.calls.iter().map(|c| c.callee.as_str()).collect();
754 assert!(
756 callees.contains(&"MapDataManager"),
757 "expected instantiation to reference class, got {callees:?}"
758 );
759 assert!(
760 callees.contains(&"load_map_data"),
761 "expected method call, got {callees:?}"
762 );
763 assert!(
764 callees.contains(&"update_state"),
765 "expected direct call, got {callees:?}"
766 );
767 }
768}