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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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}