Skip to main content

tessera_codegraph/
indexer.rs

1use std::collections::HashSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::time::Instant;
5
6use anyhow::{anyhow, Context, Result};
7use rusqlite::Connection;
8use sha2::{Digest, Sha256};
9use tree_sitter::{Node, Parser};
10use walkdir::{DirEntry, WalkDir};
11
12use crate::bloom::BloomFilter;
13use crate::config::TesseraConfig;
14use crate::db;
15use crate::snapshot;
16use crate::types::{IndexedImport, IndexedReference, IndexedSymbol, Language};
17
18#[derive(Debug, Clone)]
19pub struct IndexReport {
20    pub files_indexed: usize,
21    pub files_reused: usize,
22    pub files_removed: usize,
23    pub symbols_indexed: usize,
24    pub references_indexed: usize,
25    pub elapsed_ms: u128,
26    pub mode: IndexMode,
27    pub warnings: Vec<IndexWarning>,
28}
29
30#[derive(Debug, Clone)]
31pub struct IndexWarning {
32    pub path: String,
33    pub message: String,
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum IndexMode {
38    Full,
39    Incremental,
40}
41
42#[derive(Debug, Clone, Copy)]
43pub struct IndexOptions {
44    pub full: bool,
45    pub build_snapshot: bool,
46}
47
48impl Default for IndexOptions {
49    fn default() -> Self {
50        Self {
51            full: false,
52            build_snapshot: true,
53        }
54    }
55}
56
57pub fn index_path(root: &Path, db_path: &Path) -> Result<IndexReport> {
58    index_path_with(root, db_path, IndexOptions::default())
59}
60
61pub fn index_path_with(root: &Path, db_path: &Path, options: IndexOptions) -> Result<IndexReport> {
62    let started = Instant::now();
63    let root = root
64        .canonicalize()
65        .with_context(|| format!("failed to canonicalize {}", root.display()))?;
66    let conn = db::open(db_path)?;
67    let config = TesseraConfig::load(&root);
68    if options.full {
69        db::reset(&conn)?;
70    }
71    db::set_meta(&conn, "root", &root.to_string_lossy())?;
72
73    let mode = if options.full {
74        IndexMode::Full
75    } else {
76        IndexMode::Incremental
77    };
78
79    let mut report = IndexReport {
80        files_indexed: 0,
81        files_reused: 0,
82        files_removed: 0,
83        symbols_indexed: 0,
84        references_indexed: 0,
85        elapsed_ms: 0,
86        mode,
87        warnings: Vec::new(),
88    };
89
90    let mut visited_ids: Vec<i64> = Vec::new();
91
92    let tx = conn.unchecked_transaction()?;
93
94    for entry_result in WalkDir::new(&root)
95        .into_iter()
96        .filter_entry(|entry| should_enter(entry, &config))
97    {
98        let entry = match entry_result {
99            Ok(entry) => entry,
100            Err(error) => {
101                report.warnings.push(IndexWarning {
102                    path: error
103                        .path()
104                        .map(|p| p.display().to_string())
105                        .unwrap_or_else(|| "<walkdir>".to_string()),
106                    message: error.to_string(),
107                });
108                continue;
109            }
110        };
111        if !entry.file_type().is_file() {
112            continue;
113        }
114
115        let path = entry.path();
116        let rel_path = path
117            .strip_prefix(&root)
118            .unwrap_or(path)
119            .to_string_lossy()
120            .replace('\\', "/");
121
122        if !config.includes_path(&rel_path) || config.excludes_path(&rel_path) {
123            continue;
124        }
125
126        let Some(language) = language_for_path(path) else {
127            continue;
128        };
129        let content = match fs::read_to_string(path) {
130            Ok(c) => c,
131            Err(error) => {
132                report.warnings.push(IndexWarning {
133                    path: rel_path,
134                    message: format!("failed to read file: {error}"),
135                });
136                continue;
137            }
138        };
139        let sha = format!("{:x}", Sha256::digest(content.as_bytes()));
140
141        // Incremental: short-circuit when sha is unchanged.
142        if !options.full {
143            if let Some((existing_id, existing_sha)) = db::file_sha(&tx, &rel_path)? {
144                if existing_sha == sha {
145                    visited_ids.push(existing_id);
146                    report.files_reused += 1;
147                    continue;
148                }
149                db::delete_file_cascade(&tx, existing_id)?;
150            }
151        }
152
153        let parsed = match parse_file(language, &content) {
154            Ok(parsed) => parsed,
155            Err(error) => {
156                report.warnings.push(IndexWarning {
157                    path: rel_path,
158                    message: format!("failed to parse {language}: {error}"),
159                });
160                continue;
161            }
162        };
163
164        let file_id = db::insert_file(&tx, &rel_path, language, &sha, content.lines().count())?;
165        let symbol_ids = db::insert_symbols(&tx, file_id, &parsed.symbols)?;
166        let ref_count = db::insert_references(&tx, file_id, &parsed.references)?;
167        let _ = db::insert_imports(&tx, file_id, &parsed.imports)?;
168
169        visited_ids.push(file_id);
170        report.files_indexed += 1;
171        report.symbols_indexed += symbol_ids.len();
172        report.references_indexed += ref_count;
173    }
174
175    // Reconcile deletes only in incremental mode (full mode already reset).
176    if !options.full {
177        report.files_removed = db::delete_files_not_in(&tx, &visited_ids)?;
178    }
179
180    tx.commit()?;
181
182    rebuild_bloom(&conn)?;
183
184    if options.build_snapshot {
185        let snapshot_path = snapshot_path(db_path);
186        let _ = snapshot::build(&conn, &snapshot_path);
187    }
188
189    report.elapsed_ms = started.elapsed().as_millis();
190    Ok(report)
191}
192
193fn snapshot_path(db_path: &Path) -> PathBuf {
194    db_path
195        .parent()
196        .unwrap_or_else(|| Path::new("."))
197        .join("snapshot.bin")
198}
199
200fn rebuild_bloom(conn: &Connection) -> Result<()> {
201    let mut stmt = conn.prepare("SELECT name, qualified_name FROM symbols")?;
202    let mut names: Vec<String> = Vec::new();
203    let rows = stmt.query_map([], |row| {
204        let name: String = row.get(0)?;
205        let qualified: String = row.get(1)?;
206        Ok((name, qualified))
207    })?;
208    for row in rows {
209        let (name, qualified) = row?;
210        names.push(name);
211        names.push(qualified);
212    }
213    let expected = names.len().max(64);
214    let mut bloom = BloomFilter::for_expected(expected, 0.01);
215    for name in &names {
216        bloom.insert(name);
217    }
218    let bytes = bloom.to_bytes();
219    db::set_meta_blob(conn, "bloom_symbols", &bytes)?;
220    Ok(())
221}
222
223fn should_enter(entry: &DirEntry, config: &TesseraConfig) -> bool {
224    let name = entry.file_name().to_string_lossy();
225    if config.ignores_name(&name) {
226        return false;
227    }
228    !matches!(
229        name.as_ref(),
230        ".git"
231            | ".hg"
232            | ".svn"
233            | "node_modules"
234            | "target"
235            | "dist"
236            | "build"
237            | "out"
238            | "coverage"
239            | "vendor"
240            | "vendors"
241            | "third_party"
242            | ".cache"
243            | ".gradle"
244            | ".idea"
245            | ".pytest_cache"
246            | ".mypy_cache"
247            | ".ruff_cache"
248            | ".tox"
249            | ".cargo"
250            | "Pods"
251            | "DerivedData"
252            | ".next"
253            | ".venv"
254            | "venv"
255            | "__pycache__"
256            | ".tessera"
257    )
258}
259
260fn language_for_path(path: &Path) -> Option<Language> {
261    path.extension()
262        .and_then(|ext| ext.to_str())
263        .and_then(Language::from_extension)
264}
265
266#[derive(Debug)]
267pub struct ParsedFile {
268    pub symbols: Vec<IndexedSymbol>,
269    pub references: Vec<IndexedReference>,
270    pub imports: Vec<IndexedImport>,
271}
272
273pub fn parse_file(language: Language, content: &str) -> Result<ParsedFile> {
274    let mut parser = Parser::new();
275    match language {
276        Language::JavaScript => parser.set_language(tree_sitter_javascript::language())?,
277        // TSX grammar is a superset of TS — using it for both means a plain
278        // `.ts` file still parses cleanly *and* JSX-bearing `.tsx` files yield
279        // jsx_element nodes for React-component reference extraction.
280        Language::TypeScript | Language::Tsx => {
281            parser.set_language(tree_sitter_typescript::language_tsx())?
282        }
283        Language::Python => parser.set_language(tree_sitter_python::language())?,
284        Language::Go => parser.set_language(tree_sitter_go::language())?,
285        Language::Rust => parser.set_language(tree_sitter_rust::language())?,
286        Language::Java => parser.set_language(tree_sitter_java::language())?,
287        Language::C => parser.set_language(tree_sitter_c::language())?,
288        // The C++ grammar is a superset of C, so `.h`/C files routed here parse
289        // cleanly too.
290        Language::Cpp => parser.set_language(tree_sitter_cpp::language())?,
291        Language::CSharp => parser.set_language(tree_sitter_c_sharp::language())?,
292        Language::Ruby => parser.set_language(tree_sitter_ruby::language())?,
293        Language::Php => parser.set_language(tree_sitter_php::language())?,
294    }
295    let tree = parser
296        .parse(content, None)
297        .ok_or_else(|| anyhow!("tree-sitter returned no parse tree"))?;
298    let mut visitor = Visitor::new(language, content);
299    visitor.walk(tree.root_node(), None);
300    Ok(ParsedFile {
301        symbols: visitor.symbols,
302        references: visitor.references,
303        imports: visitor.imports,
304    })
305}
306
307fn is_js_family(lang: Language) -> bool {
308    matches!(
309        lang,
310        Language::JavaScript | Language::TypeScript | Language::Tsx
311    )
312}
313
314struct Visitor<'a> {
315    language: Language,
316    content: &'a str,
317    lines: Vec<&'a str>,
318    scope: Vec<ScopeFrame>,
319    symbols: Vec<IndexedSymbol>,
320    references: Vec<IndexedReference>,
321    imports: Vec<IndexedImport>,
322    skip_refs: HashSet<&'static str>,
323}
324
325#[derive(Debug, Clone)]
326struct ScopeFrame {
327    name: String,
328    end_byte: usize,
329}
330
331impl<'a> Visitor<'a> {
332    fn new(language: Language, content: &'a str) -> Self {
333        let skip_refs: HashSet<&'static str> = match language {
334            Language::JavaScript | Language::TypeScript | Language::Tsx => {
335                ["super"].into_iter().collect()
336            }
337            Language::Python => HashSet::new(),
338            Language::Go => HashSet::new(),
339            Language::Rust => [
340                "println",
341                "eprintln",
342                "format",
343                "vec",
344                "assert",
345                "assert_eq",
346            ]
347            .into_iter()
348            .collect(),
349            Language::Java => ["super", "this"].into_iter().collect(),
350            // C / C++: the noisiest "calls" are libc / control macros that
351            // would never resolve to an indexed symbol.
352            Language::C | Language::Cpp => [
353                "sizeof",
354                "printf",
355                "fprintf",
356                "sprintf",
357                "malloc",
358                "free",
359                "memcpy",
360                "memset",
361                "strlen",
362                "strcmp",
363                "assert",
364                "static_cast",
365                "reinterpret_cast",
366                "dynamic_cast",
367                "const_cast",
368            ]
369            .into_iter()
370            .collect(),
371            Language::CSharp => ["base", "this", "nameof", "typeof"].into_iter().collect(),
372            Language::Ruby => [
373                "require",
374                "require_relative",
375                "load",
376                "puts",
377                "print",
378                "p",
379                "raise",
380            ]
381            .into_iter()
382            .collect(),
383            Language::Php => [
384                "require",
385                "require_once",
386                "include",
387                "include_once",
388                "echo",
389                "isset",
390            ]
391            .into_iter()
392            .collect(),
393        };
394        Self {
395            language,
396            content,
397            lines: content.lines().collect(),
398            scope: Vec::new(),
399            symbols: Vec::new(),
400            references: Vec::new(),
401            imports: Vec::new(),
402            skip_refs,
403        }
404    }
405
406    fn walk(&mut self, node: Node<'a>, pending_export: Option<bool>) {
407        while self
408            .scope
409            .last()
410            .map(|scope| node.start_byte() >= scope.end_byte)
411            .unwrap_or(false)
412        {
413            self.scope.pop();
414        }
415
416        if is_js_family(self.language) && node.kind() == "export_statement" {
417            let mut cursor = node.walk();
418            for child in node.children(&mut cursor) {
419                self.walk(child, Some(true));
420            }
421            return;
422        }
423
424        if let Some(symbol) = self.symbol_from_node(node, pending_export.unwrap_or(false)) {
425            let qname = symbol.qualified_name.clone();
426            let end_byte = node.end_byte();
427            self.symbols.push(symbol);
428            self.scope.push(ScopeFrame {
429                name: qname,
430                end_byte,
431            });
432        }
433
434        if let Some(reference) = self.reference_from_node(node) {
435            self.references.push(reference);
436        }
437
438        if let Some(import) = self.import_from_node(node) {
439            self.imports.push(import);
440        }
441
442        let mut cursor = node.walk();
443        for child in node.children(&mut cursor) {
444            self.walk(child, None);
445        }
446    }
447
448    fn symbol_from_node(&self, node: Node<'a>, exported: bool) -> Option<IndexedSymbol> {
449        match self.language {
450            Language::Python => self.python_symbol_from_node(node),
451            Language::JavaScript | Language::TypeScript | Language::Tsx => {
452                self.js_ts_symbol_from_node(node, exported)
453            }
454            Language::Go => self.go_symbol_from_node(node),
455            Language::Rust => self.rust_symbol_from_node(node),
456            Language::Java => self.java_symbol_from_node(node),
457            Language::C => self.c_family_symbol_from_node(node, false),
458            Language::Cpp => self.c_family_symbol_from_node(node, true),
459            Language::CSharp => self.csharp_symbol_from_node(node),
460            Language::Ruby => self.ruby_symbol_from_node(node),
461            Language::Php => self.php_symbol_from_node(node),
462        }
463    }
464
465    fn python_symbol_from_node(&self, node: Node<'a>) -> Option<IndexedSymbol> {
466        let kind = match node.kind() {
467            "function_definition" => "function",
468            "class_definition" => "class",
469            _ => return None,
470        };
471        let name_node = node.child_by_field_name("name")?;
472        let name = self.node_text(name_node);
473        let signature = self.signature_until_body(node);
474        Some(self.make_symbol(name, kind, node, signature, true))
475    }
476
477    fn js_ts_symbol_from_node(&self, node: Node<'a>, exported: bool) -> Option<IndexedSymbol> {
478        let (name, kind) = match node.kind() {
479            "function_declaration" | "generator_function_declaration" => (
480                self.node_text(node.child_by_field_name("name")?),
481                "function",
482            ),
483            "class_declaration" => (self.node_text(node.child_by_field_name("name")?), "class"),
484            "method_definition" | "method_signature" => {
485                (self.node_text(node.child_by_field_name("name")?), "method")
486            }
487            "variable_declarator" => {
488                let value = node.child_by_field_name("value")?;
489                if !matches!(
490                    value.kind(),
491                    "arrow_function" | "function" | "function_expression"
492                ) {
493                    return None;
494                }
495                (
496                    self.node_text(node.child_by_field_name("name")?),
497                    "function",
498                )
499            }
500            _ => return None,
501        };
502        let signature = self.signature_until_body(node);
503        Some(self.make_symbol(
504            name,
505            kind,
506            node,
507            signature,
508            exported || self.has_export_ancestor(node),
509        ))
510    }
511
512    fn go_symbol_from_node(&self, node: Node<'a>) -> Option<IndexedSymbol> {
513        match node.kind() {
514            "function_declaration" => {
515                let name_node = node.child_by_field_name("name")?;
516                let name = self.node_text(name_node);
517                let signature = self.signature_until_body(node);
518                let exported = is_go_exported(&name);
519                Some(self.make_symbol(name, "function", node, signature, exported))
520            }
521            "method_declaration" => {
522                let name_node = node.child_by_field_name("name")?;
523                let raw_name = self.node_text(name_node);
524                let receiver_type = node
525                    .child_by_field_name("receiver")
526                    .and_then(|recv| self.go_receiver_type(recv));
527                let qualified = receiver_type
528                    .as_ref()
529                    .map(|t| format!("{}.{}", t, raw_name))
530                    .unwrap_or_else(|| raw_name.clone());
531                let signature = self.signature_until_body(node);
532                let exported = is_go_exported(&raw_name);
533                Some(IndexedSymbol {
534                    name: raw_name,
535                    qualified_name: qualified,
536                    kind: "method".to_string(),
537                    start_line: node.start_position().row + 1,
538                    end_line: node.end_position().row + 1,
539                    signature,
540                    exported,
541                })
542            }
543            "type_spec" => {
544                let name_node = node.child_by_field_name("name")?;
545                let name = self.node_text(name_node);
546                let type_node = node.child_by_field_name("type")?;
547                let kind = match type_node.kind() {
548                    "struct_type" => "struct",
549                    "interface_type" => "interface",
550                    _ => "type",
551                };
552                let signature = self.signature_until_body(node);
553                let exported = is_go_exported(&name);
554                Some(self.make_symbol(name, kind, node, signature, exported))
555            }
556            _ => None,
557        }
558    }
559
560    fn go_receiver_type(&self, recv: Node<'a>) -> Option<String> {
561        // receiver is parameter_list; walk for the first identifier inside a type
562        let mut cursor = recv.walk();
563        for child in recv.children(&mut cursor) {
564            if child.kind() == "parameter_declaration" {
565                if let Some(type_node) = child.child_by_field_name("type") {
566                    return Some(self.unwrap_go_type(type_node));
567                }
568            }
569        }
570        None
571    }
572
573    fn unwrap_go_type(&self, node: Node<'a>) -> String {
574        match node.kind() {
575            "pointer_type" => {
576                if let Some(inner) = node.named_child(0) {
577                    self.unwrap_go_type(inner)
578                } else {
579                    self.node_text(node)
580                }
581            }
582            "type_identifier" | "identifier" | "qualified_type" => self.node_text(node),
583            _ => self.node_text(node),
584        }
585    }
586
587    fn java_symbol_from_node(&self, node: Node<'a>) -> Option<IndexedSymbol> {
588        let (name, kind) = match node.kind() {
589            "class_declaration" => (self.node_text(node.child_by_field_name("name")?), "class"),
590            "interface_declaration" => (
591                self.node_text(node.child_by_field_name("name")?),
592                "interface",
593            ),
594            "enum_declaration" => (self.node_text(node.child_by_field_name("name")?), "enum"),
595            "record_declaration" => (self.node_text(node.child_by_field_name("name")?), "record"),
596            "annotation_type_declaration" => (
597                self.node_text(node.child_by_field_name("name")?),
598                "annotation",
599            ),
600            "method_declaration" => (self.node_text(node.child_by_field_name("name")?), "method"),
601            "constructor_declaration" => (
602                self.node_text(node.child_by_field_name("name")?),
603                "constructor",
604            ),
605            _ => return None,
606        };
607        let signature = self.signature_until_body(node);
608        let exported = java_is_public(node);
609        Some(self.make_symbol(name, kind, node, signature, exported))
610    }
611
612    fn rust_symbol_from_node(&self, node: Node<'a>) -> Option<IndexedSymbol> {
613        let (name, kind) = match node.kind() {
614            "function_item" => (
615                self.node_text(node.child_by_field_name("name")?),
616                "function",
617            ),
618            "struct_item" => (self.node_text(node.child_by_field_name("name")?), "struct"),
619            "enum_item" => (self.node_text(node.child_by_field_name("name")?), "enum"),
620            "trait_item" => (self.node_text(node.child_by_field_name("name")?), "trait"),
621            "mod_item" => (self.node_text(node.child_by_field_name("name")?), "module"),
622            "impl_item" => {
623                let type_node = node.child_by_field_name("type")?;
624                let name = self.node_text(type_node);
625                let signature = self.signature_until_body(node);
626                return Some(self.make_symbol(name, "impl", node, signature, false));
627            }
628            _ => return None,
629        };
630        let signature = self.signature_until_body(node);
631        let exported = rust_is_pub(node);
632        Some(self.make_symbol(name, kind, node, signature, exported))
633    }
634
635    /// C and C++ share one extractor; `cpp` enables the C++-only kinds
636    /// (`class_specifier`, `namespace_definition`). The C++ grammar is a
637    /// superset of C, so this also covers `.h` headers.
638    fn c_family_symbol_from_node(&self, node: Node<'a>, cpp: bool) -> Option<IndexedSymbol> {
639        match node.kind() {
640            "function_definition" => {
641                let decl = node.child_by_field_name("declarator")?;
642                let name = self.c_declarator_name(decl)?;
643                if name.is_empty() {
644                    return None;
645                }
646                let signature = self.signature_until_body(node);
647                let exported = !self.c_has_static(node);
648                Some(self.make_symbol(name, "function", node, signature, exported))
649            }
650            "struct_specifier" if node.child_by_field_name("body").is_some() => {
651                let name = self.node_text(node.child_by_field_name("name")?);
652                let signature = self.signature_until_body(node);
653                Some(self.make_symbol(name, "struct", node, signature, true))
654            }
655            "enum_specifier" if node.child_by_field_name("body").is_some() => {
656                let name = self.node_text(node.child_by_field_name("name")?);
657                let signature = self.signature_until_body(node);
658                Some(self.make_symbol(name, "enum", node, signature, true))
659            }
660            "union_specifier" if node.child_by_field_name("body").is_some() => {
661                let name = self.node_text(node.child_by_field_name("name")?);
662                let signature = self.signature_until_body(node);
663                Some(self.make_symbol(name, "union", node, signature, true))
664            }
665            "class_specifier" if cpp && node.child_by_field_name("body").is_some() => {
666                let name = self.node_text(node.child_by_field_name("name")?);
667                let signature = self.signature_until_body(node);
668                Some(self.make_symbol(name, "class", node, signature, true))
669            }
670            "namespace_definition" if cpp => {
671                let name = self.node_text(node.child_by_field_name("name")?);
672                let signature = self.signature_until_body(node);
673                Some(self.make_symbol(name, "namespace", node, signature, true))
674            }
675            _ => None,
676        }
677    }
678
679    /// Unwrap a C/C++ declarator (pointer/reference/array/function wrappers)
680    /// down to the innermost identifier or qualified name.
681    fn c_declarator_name(&self, node: Node<'a>) -> Option<String> {
682        match node.kind() {
683            "identifier" | "field_identifier" | "type_identifier" | "destructor_name"
684            | "operator_name" => Some(self.node_text(node)),
685            "qualified_identifier" => node
686                .child_by_field_name("name")
687                .map(|n| self.node_text(n))
688                .or_else(|| Some(last_identifier(self.node_text(node)))),
689            "function_declarator"
690            | "pointer_declarator"
691            | "reference_declarator"
692            | "parenthesized_declarator"
693            | "array_declarator"
694            | "init_declarator" => {
695                let inner = node.child_by_field_name("declarator")?;
696                self.c_declarator_name(inner)
697            }
698            _ => {
699                let mut cursor = node.walk();
700                for child in node.children(&mut cursor) {
701                    if let Some(name) = self.c_declarator_name(child) {
702                        return Some(name);
703                    }
704                }
705                None
706            }
707        }
708    }
709
710    fn c_has_static(&self, node: Node<'a>) -> bool {
711        let mut cursor = node.walk();
712        for child in node.children(&mut cursor) {
713            if child.kind() == "storage_class_specifier" && self.node_text(child) == "static" {
714                return true;
715            }
716        }
717        false
718    }
719
720    fn csharp_symbol_from_node(&self, node: Node<'a>) -> Option<IndexedSymbol> {
721        let (name, kind) = match node.kind() {
722            "class_declaration" => (self.node_text(node.child_by_field_name("name")?), "class"),
723            "interface_declaration" => (
724                self.node_text(node.child_by_field_name("name")?),
725                "interface",
726            ),
727            "struct_declaration" => (self.node_text(node.child_by_field_name("name")?), "struct"),
728            "enum_declaration" => (self.node_text(node.child_by_field_name("name")?), "enum"),
729            "record_declaration" => (self.node_text(node.child_by_field_name("name")?), "record"),
730            "method_declaration" => (self.node_text(node.child_by_field_name("name")?), "method"),
731            "constructor_declaration" => (
732                self.node_text(node.child_by_field_name("name")?),
733                "constructor",
734            ),
735            _ => return None,
736        };
737        let signature = self.signature_until_body(node);
738        let exported = self.csharp_is_public(node);
739        Some(self.make_symbol(name, kind, node, signature, exported))
740    }
741
742    fn csharp_is_public(&self, node: Node<'a>) -> bool {
743        let mut cursor = node.walk();
744        for child in node.children(&mut cursor) {
745            if child.kind() == "modifier" && self.node_text(child) == "public" {
746                return true;
747            }
748        }
749        false
750    }
751
752    fn ruby_symbol_from_node(&self, node: Node<'a>) -> Option<IndexedSymbol> {
753        let (name, kind) = match node.kind() {
754            "method" | "singleton_method" => {
755                (self.node_text(node.child_by_field_name("name")?), "method")
756            }
757            "class" => (self.node_text(node.child_by_field_name("name")?), "class"),
758            "module" => (self.node_text(node.child_by_field_name("name")?), "module"),
759            _ => return None,
760        };
761        // Ruby methods are public by default.
762        let signature = self.signature_until_body(node);
763        Some(self.make_symbol(name, kind, node, signature, true))
764    }
765
766    fn php_symbol_from_node(&self, node: Node<'a>) -> Option<IndexedSymbol> {
767        let (name, kind) = match node.kind() {
768            "function_definition" => (
769                self.node_text(node.child_by_field_name("name")?),
770                "function",
771            ),
772            "method_declaration" => (self.node_text(node.child_by_field_name("name")?), "method"),
773            "class_declaration" => (self.node_text(node.child_by_field_name("name")?), "class"),
774            "interface_declaration" => (
775                self.node_text(node.child_by_field_name("name")?),
776                "interface",
777            ),
778            "trait_declaration" => (self.node_text(node.child_by_field_name("name")?), "trait"),
779            "enum_declaration" => (self.node_text(node.child_by_field_name("name")?), "enum"),
780            _ => return None,
781        };
782        let signature = self.signature_until_body(node);
783        let exported = self.php_is_public(node);
784        Some(self.make_symbol(name, kind, node, signature, exported))
785    }
786
787    fn php_is_public(&self, node: Node<'a>) -> bool {
788        // Top-level functions are global; methods default to public unless a
789        // `visibility_modifier` says private/protected.
790        if node.kind() != "method_declaration" {
791            return true;
792        }
793        let mut cursor = node.walk();
794        let mut saw = false;
795        let mut public = false;
796        for child in node.children(&mut cursor) {
797            if child.kind() == "visibility_modifier" {
798                saw = true;
799                if self.node_text(child).eq_ignore_ascii_case("public") {
800                    public = true;
801                }
802            }
803        }
804        !saw || public
805    }
806
807    fn make_symbol(
808        &self,
809        name: String,
810        kind: &str,
811        node: Node<'a>,
812        signature: String,
813        exported: bool,
814    ) -> IndexedSymbol {
815        let qualified_name = self
816            .scope
817            .last()
818            .map(|parent| format!("{}.{}", parent.name, name))
819            .unwrap_or_else(|| name.clone());
820        IndexedSymbol {
821            name,
822            qualified_name,
823            kind: kind.to_string(),
824            start_line: node.start_position().row + 1,
825            end_line: node.end_position().row + 1,
826            signature,
827            exported,
828        }
829    }
830
831    fn reference_from_node(&self, node: Node<'a>) -> Option<IndexedReference> {
832        let kind = node.kind();
833        // JSX is its own case because the "callable" isn't a function in the
834        // grammar — it's the element name in <Component … />. Only register
835        // names that look like React components (leading uppercase or
836        // member-expression like `Foo.Bar`); lowercase identifiers are intrinsic
837        // HTML elements and would just be noise.
838        if is_js_family(self.language)
839            && matches!(kind, "jsx_self_closing_element" | "jsx_opening_element")
840        {
841            let name_node = node.child_by_field_name("name")?;
842            let symbol_name = self.jsx_component_name(name_node)?;
843            return Some(IndexedReference {
844                symbol_name,
845                from_qualified_name: self.scope.last().map(|scope| scope.name.clone()),
846                line: node.start_position().row + 1,
847                column: node.start_position().column + 1,
848                context: self
849                    .lines
850                    .get(node.start_position().row)
851                    .map(|line| line.trim().to_string())
852                    .unwrap_or_default(),
853                kind: "jsx".to_string(),
854            });
855        }
856
857        let (function_node, ref_kind) = match (self.language, kind) {
858            (Language::Python, "call") => (node.child_by_field_name("function")?, "call"),
859            (Language::JavaScript | Language::TypeScript | Language::Tsx, "call_expression") => {
860                (node.child_by_field_name("function")?, "call")
861            }
862            (Language::Go, "call_expression") => (node.child_by_field_name("function")?, "call"),
863            (Language::Rust, "call_expression") => (node.child_by_field_name("function")?, "call"),
864            (Language::Rust, "macro_invocation") => (node.child_by_field_name("macro")?, "macro"),
865            (Language::Java, "method_invocation") => (node.child_by_field_name("name")?, "call"),
866            (Language::Java, "object_creation_expression") => {
867                (node.child_by_field_name("type")?, "new")
868            }
869            (Language::C | Language::Cpp, "call_expression") => {
870                (node.child_by_field_name("function")?, "call")
871            }
872            (Language::Cpp, "new_expression") => (
873                node.child_by_field_name("type").or_else(|| {
874                    self.first_child_with_kinds(node, &["type_identifier", "qualified_identifier"])
875                })?,
876                "new",
877            ),
878            (Language::CSharp, "invocation_expression") => {
879                (node.child_by_field_name("function")?, "call")
880            }
881            (Language::CSharp, "object_creation_expression") => {
882                (node.child_by_field_name("type")?, "new")
883            }
884            (Language::Ruby, "call") => (node.child_by_field_name("method")?, "call"),
885            (Language::Ruby, "method_call") => (node.child_by_field_name("method")?, "call"),
886            (Language::Php, "function_call_expression") => {
887                (node.child_by_field_name("function")?, "call")
888            }
889            (Language::Php, "member_call_expression") => {
890                (node.child_by_field_name("name")?, "call")
891            }
892            (Language::Php, "scoped_call_expression") => {
893                (node.child_by_field_name("name")?, "call")
894            }
895            (Language::Php, "object_creation_expression") => (
896                self.first_child_with_kinds(node, &["name", "qualified_name"])?,
897                "new",
898            ),
899            _ => return None,
900        };
901        let symbol_name = self.called_name(function_node)?;
902        if self.skip_refs.contains(symbol_name.as_str()) {
903            return None;
904        }
905        Some(IndexedReference {
906            symbol_name,
907            from_qualified_name: self.scope.last().map(|scope| scope.name.clone()),
908            line: node.start_position().row + 1,
909            column: node.start_position().column + 1,
910            context: self
911                .lines
912                .get(node.start_position().row)
913                .map(|line| line.trim().to_string())
914                .unwrap_or_default(),
915            kind: ref_kind.to_string(),
916        })
917    }
918
919    fn import_from_node(&self, node: Node<'a>) -> Option<IndexedImport> {
920        // Per-language import / use statements. We capture the source/path
921        // string (or, for Rust `use foo::bar`, the full path text) so that
922        // `imports(path)` and `imported_by(source)` can answer module-level
923        // dependency questions without re-reading the file.
924        let (source, kind) = match (self.language, node.kind()) {
925            (Language::JavaScript | Language::TypeScript | Language::Tsx, "import_statement") => {
926                let src = node
927                    .child_by_field_name("source")
928                    .or_else(|| self.first_child_with_kinds(node, &["string"]))?;
929                (self.strip_quotes(self.node_text(src)), "import")
930            }
931            (Language::JavaScript | Language::TypeScript | Language::Tsx, "call_expression") => {
932                // CommonJS `require('./foo')` and dynamic ESM `import('./foo')`.
933                // Both look like call_expressions in the grammar; only record
934                // them when the callee is exactly `require` or `import`, with
935                // a single string argument.
936                let fn_node = node.child_by_field_name("function")?;
937                let name = self.node_text(fn_node);
938                if name != "require" && name != "import" {
939                    return None;
940                }
941                let args = node.child_by_field_name("arguments")?;
942                let string_arg = self.first_child_with_kinds(args, &["string"])?;
943                let kind = if name == "require" {
944                    "require"
945                } else {
946                    "import"
947                };
948                (self.strip_quotes(self.node_text(string_arg)), kind)
949            }
950            (Language::Python, "import_statement") => {
951                let name_node = node.child_by_field_name("name").or_else(|| {
952                    self.first_child_with_kinds(node, &["dotted_name", "aliased_import"])
953                })?;
954                (self.node_text(name_node), "import")
955            }
956            (Language::Python, "import_from_statement") => {
957                let module = node.child_by_field_name("module_name")?;
958                (self.node_text(module), "from")
959            }
960            (Language::Go, "import_spec") => {
961                let path_node = node.child_by_field_name("path").or_else(|| {
962                    self.first_child_with_kinds(node, &["interpreted_string_literal"])
963                })?;
964                (self.strip_quotes(self.node_text(path_node)), "import")
965            }
966            (Language::Rust, "use_declaration") => {
967                // tree-sitter-rust exposes the argument as a "argument" field
968                // holding a path-like node (scoped_identifier, use_list, etc.).
969                let arg = node
970                    .child_by_field_name("argument")
971                    .or_else(|| self.first_named_child(node))?;
972                (self.node_text(arg), "use")
973            }
974            (Language::Java, "import_declaration") => {
975                let target = self.first_child_with_kinds(
976                    node,
977                    &["scoped_identifier", "identifier", "asterisk"],
978                )?;
979                (self.node_text(target), "import")
980            }
981            (Language::C | Language::Cpp, "preproc_include") => {
982                let path = node.child_by_field_name("path").or_else(|| {
983                    self.first_child_with_kinds(node, &["string_literal", "system_lib_string"])
984                })?;
985                let cleaned = self
986                    .node_text(path)
987                    .trim_matches(|c: char| matches!(c, '"' | '<' | '>' | ' '))
988                    .to_string();
989                (cleaned, "include")
990            }
991            (Language::CSharp, "using_directive") => {
992                let name = node
993                    .child_by_field_name("name")
994                    .or_else(|| self.first_named_child(node))?;
995                (self.node_text(name), "using")
996            }
997            (Language::Ruby, "call") => {
998                // require / require_relative / load 'path'
999                let method = node.child_by_field_name("method")?;
1000                let mname = self.node_text(method);
1001                if mname != "require" && mname != "require_relative" && mname != "load" {
1002                    return None;
1003                }
1004                let args = node.child_by_field_name("arguments")?;
1005                let string_arg =
1006                    self.first_child_with_kinds(args, &["string", "string_content"])?;
1007                (self.strip_quotes(self.node_text(string_arg)), "require")
1008            }
1009            (Language::Php, "namespace_use_declaration") => {
1010                let clause = self
1011                    .first_child_with_kinds(node, &["namespace_use_clause"])
1012                    .unwrap_or(node);
1013                let name = self.first_child_with_kinds(
1014                    clause,
1015                    &["qualified_name", "namespace_name", "name"],
1016                )?;
1017                (self.node_text(name), "use")
1018            }
1019            _ => return None,
1020        };
1021        if source.is_empty() {
1022            return None;
1023        }
1024        Some(IndexedImport {
1025            source,
1026            line: node.start_position().row + 1,
1027            kind: kind.to_string(),
1028        })
1029    }
1030
1031    fn strip_quotes(&self, s: String) -> String {
1032        s.trim_matches(|c: char| c == '"' || c == '\'' || c == '`')
1033            .to_string()
1034    }
1035
1036    fn first_child_with_kinds(&self, node: Node<'a>, kinds: &[&str]) -> Option<Node<'a>> {
1037        let mut cursor = node.walk();
1038        let mut found: Option<Node<'a>> = None;
1039        for child in node.children(&mut cursor) {
1040            if kinds.contains(&child.kind()) {
1041                found = Some(child);
1042                break;
1043            }
1044        }
1045        found
1046    }
1047
1048    fn first_named_child(&self, node: Node<'a>) -> Option<Node<'a>> {
1049        let mut cursor = node.walk();
1050        let mut found: Option<Node<'a>> = None;
1051        for child in node.children(&mut cursor) {
1052            if child.is_named() {
1053                found = Some(child);
1054                break;
1055            }
1056        }
1057        found
1058    }
1059
1060    fn jsx_component_name(&self, name_node: Node<'a>) -> Option<String> {
1061        match name_node.kind() {
1062            "identifier" => {
1063                let text = self.node_text(name_node);
1064                let first = text.chars().next()?;
1065                if first.is_ascii_uppercase() || first == '_' {
1066                    Some(text)
1067                } else {
1068                    None
1069                }
1070            }
1071            "nested_identifier" | "member_expression" => {
1072                // <Foo.Bar /> — record the rightmost segment as the component.
1073                let last = name_node
1074                    .child_by_field_name("property")
1075                    .or_else(|| name_node.child_by_field_name("name"))
1076                    .or_else(|| {
1077                        name_node.named_child(name_node.named_child_count().saturating_sub(1))
1078                    })?;
1079                Some(self.node_text(last))
1080            }
1081            _ => {
1082                let text = self.node_text(name_node);
1083                let first = text.chars().next()?;
1084                if first.is_ascii_uppercase() {
1085                    Some(text)
1086                } else {
1087                    None
1088                }
1089            }
1090        }
1091    }
1092
1093    fn called_name(&self, node: Node<'a>) -> Option<String> {
1094        match node.kind() {
1095            "identifier" | "property_identifier" | "type_identifier" => Some(self.node_text(node)),
1096            "attribute" => node
1097                .child_by_field_name("attribute")
1098                .map(|child| self.node_text(child))
1099                .or_else(|| Some(last_identifier(self.node_text(node)))),
1100            "member_expression" | "selector_expression" | "member_access_expression" => node
1101                .child_by_field_name("property")
1102                .or_else(|| node.child_by_field_name("field"))
1103                .or_else(|| node.child_by_field_name("name"))
1104                .map(|child| self.node_text(child))
1105                .or_else(|| Some(last_identifier(self.node_text(node)))),
1106            "qualified_identifier" => node
1107                .child_by_field_name("name")
1108                .map(|child| self.node_text(child))
1109                .or_else(|| Some(last_identifier(self.node_text(node)))),
1110            "field_expression" => node
1111                .child_by_field_name("field")
1112                .map(|child| self.node_text(child))
1113                .or_else(|| Some(last_identifier(self.node_text(node)))),
1114            "scoped_identifier" => node
1115                .child_by_field_name("name")
1116                .map(|child| self.node_text(child))
1117                .or_else(|| Some(last_identifier(self.node_text(node)))),
1118            "subscript" => node
1119                .child_by_field_name("value")
1120                .and_then(|child| self.called_name(child)),
1121            _ => Some(last_identifier(self.node_text(node))).filter(|name| !name.is_empty()),
1122        }
1123    }
1124
1125    fn node_text(&self, node: Node<'a>) -> String {
1126        node.utf8_text(self.content.as_bytes())
1127            .unwrap_or_default()
1128            .trim()
1129            .to_string()
1130    }
1131
1132    fn signature_until_body(&self, node: Node<'a>) -> String {
1133        let text = self.node_text(node);
1134        let first_line = text.lines().next().unwrap_or_default().trim();
1135        if first_line.len() <= 180 {
1136            first_line.to_string()
1137        } else {
1138            format!("{}...", &first_line[..180])
1139        }
1140    }
1141
1142    fn has_export_ancestor(&self, node: Node<'a>) -> bool {
1143        let mut parent = node.parent();
1144        while let Some(current) = parent {
1145            if current.kind() == "export_statement" {
1146                return true;
1147            }
1148            parent = current.parent();
1149        }
1150        false
1151    }
1152}
1153
1154fn is_go_exported(name: &str) -> bool {
1155    name.chars().next().is_some_and(|c| c.is_ascii_uppercase())
1156}
1157
1158fn rust_is_pub(node: Node<'_>) -> bool {
1159    let mut cursor = node.walk();
1160    for child in node.children(&mut cursor) {
1161        if child.kind() == "visibility_modifier" {
1162            return true;
1163        }
1164    }
1165    false
1166}
1167
1168fn java_is_public(node: Node<'_>) -> bool {
1169    // Java visibility lives in a `modifiers` child. In tree-sitter-java the
1170    // `public` modifier shows up as its own typed child node, so we don't need
1171    // the source bytes — walking the modifier subtree by kind() is enough.
1172    let mut cursor = node.walk();
1173    for child in node.children(&mut cursor) {
1174        if child.kind() != "modifiers" {
1175            continue;
1176        }
1177        let mut inner = child.walk();
1178        for modifier in child.children(&mut inner) {
1179            if modifier.kind() == "public" {
1180                return true;
1181            }
1182        }
1183    }
1184    false
1185}
1186
1187fn last_identifier(text: String) -> String {
1188    text.rsplit(|ch: char| !(ch.is_ascii_alphanumeric() || ch == '_' || ch == '$'))
1189        .find(|part| !part.is_empty())
1190        .unwrap_or_default()
1191        .to_string()
1192}