1mod configured_modules;
4mod languages;
5mod resolution_keys;
6mod semantic;
7
8pub use configured_modules::{
9 ConfiguredModuleError, ConfiguredModuleResolution, EcmaScriptConfigKind,
10 EcmaScriptModuleConfig, EcmaScriptPathMapping, MAX_CONFIGURED_MODULE_CONFIGS,
11 MAX_CONFIGURED_MODULE_IDENTITY_BYTES, MAX_CONFIGURED_MODULE_MAPPINGS,
12 MAX_CONFIGURED_MODULE_TARGETS,
13};
14pub use resolution_keys::{
15 ImportReference, ImportSyntax, MAX_RESOLUTION_KEYS_PER_FACT, RelationResolutionKeys,
16 ResolutionKeyProjection, ResolutionProjectionContext, ResolutionProjectionError,
17 SEMANTIC_RESOLUTION_CONTRACT_VERSION, SymbolResolutionKeys, derive_resolution_keys,
18 derive_resolution_keys_with_context, module_aliases_for_path, parse_import_references,
19 resolve_relative_import_path, semantic_resolution_contract_digest, source_stems_for_path,
20};
21
22use projectatlas_core::language::{
23 EmbeddedHostKind, EmbeddedLanguageCapability, SymbolParserOwner, TreeSitterGrammar,
24 builtin_tree_sitter_language_ids, language_capability, tree_sitter_grammar,
25};
26use projectatlas_core::symbols::{
27 CodeSymbol, ParserKind, RelationKind, SymbolGraph, SymbolKind, SymbolRelation,
28};
29use projectatlas_core::{IndexWorkControl, IndexWorkFailure, IndexWorkStage};
30use regex::Regex;
31use std::borrow::Cow;
32use std::collections::BTreeSet;
33use std::convert::Infallible;
34use std::ops::ControlFlow;
35use std::path::Path;
36use toml::Value as TomlValue;
37use tree_sitter::{Language, Node, ParseOptions, Parser};
38
39const MAX_SYMBOLS_PER_FILE: usize = 4_000;
41const MAX_RELATIONS_PER_FILE: usize = 8_000;
43const MAX_SNIPPET_CHARS: usize = 240;
45const MAX_DOC_CHARS: usize = 500;
47const PARSER_CONTROL_CHECK_INTERVAL: usize = 128;
49
50#[must_use]
52pub fn extract_symbol_graph(path: &str, language: Option<&str>, content: &str) -> SymbolGraph {
53 match extract_symbol_graph_checked(path, language, content, &mut || Ok::<(), Infallible>(())) {
54 Ok(graph) => graph,
55 Err(unreachable) => match unreachable {},
56 }
57}
58
59pub fn extract_symbol_graph_controlled(
65 path: &str,
66 language: Option<&str>,
67 content: &str,
68 control: &IndexWorkControl,
69) -> Result<SymbolGraph, IndexWorkFailure> {
70 extract_symbol_graph_checked(path, language, content, &mut || {
71 control.check(IndexWorkStage::SymbolParsing)
72 })
73}
74
75fn extract_symbol_graph_checked<E>(
77 path: &str,
78 language: Option<&str>,
79 content: &str,
80 check: &mut impl FnMut() -> Result<(), E>,
81) -> Result<SymbolGraph, E> {
82 check()?;
83 let parse_content = content_without_leading_purpose_header(content);
84 if let Some(capability) = semantic::embedded_source::host_capability(path, language) {
85 return extract_embedded_host_graph_checked(
86 path,
87 language,
88 parse_content.as_ref(),
89 capability,
90 check,
91 );
92 }
93 match symbol_parser_owner(path, language) {
94 SymbolParserOwner::CargoManifest => {
95 return extract_cargo_manifest_graph_checked(path, language, content, check);
96 }
97 SymbolParserOwner::Vue => {
98 return extract_vue_sfc_graph_checked(path, language, parse_content.as_ref(), check);
99 }
100 SymbolParserOwner::PowerShell => {
101 return extract_powershell_graph_checked(path, language, parse_content.as_ref(), check);
102 }
103 SymbolParserOwner::Unavailable => {
104 check()?;
105 return Ok(empty_graph(path, language, ParserKind::Structural));
106 }
107 SymbolParserOwner::TreeSitter(_) | SymbolParserOwner::Fallback => {}
108 }
109 if let Some(parsed) = extract_tree_sitter_graph(path, language, parse_content.as_ref(), check)?
110 {
111 if !parsed.graph.symbols.is_empty() || !parsed.graph.relations.is_empty() {
112 check()?;
113 return Ok(parsed.graph);
114 }
115 if parsed.had_errors {
116 let fallback =
117 extract_fallback_graph_checked(path, language, parse_content.as_ref(), check)?;
118 if !fallback.symbols.is_empty() || !fallback.relations.is_empty() {
119 check()?;
120 return Ok(fallback);
121 }
122 }
123 check()?;
124 return Ok(parsed.graph);
125 }
126 extract_fallback_graph_checked(path, language, parse_content.as_ref(), check)
127}
128
129fn extract_embedded_host_graph_checked<E>(
131 path: &str,
132 language: Option<&str>,
133 content: &str,
134 capability: EmbeddedLanguageCapability,
135 check: &mut impl FnMut() -> Result<(), E>,
136) -> Result<SymbolGraph, E> {
137 let mut graph = match capability.host_kind {
138 EmbeddedHostKind::HtmlLike => empty_graph(path, language, ParserKind::Structural),
139 EmbeddedHostKind::Component => {
140 extract_vue_sfc_graph_checked(path, language, content, check)?
141 }
142 EmbeddedHostKind::Template => {
143 extract_fallback_graph_checked(path, language, content, check)?
144 }
145 };
146 let (projections, _incomplete) = semantic::embedded_source::project(content).into_parts();
147 for projection in projections {
151 check()?;
152 if let Some(parsed) = extract_tree_sitter_graph(
153 path,
154 Some(projection.language().as_str()),
155 projection.source(),
156 check,
157 )? {
158 merge_missing_graph_entries_checked(
159 &mut graph,
160 parsed.graph,
161 capability.host_kind,
162 check,
163 )?;
164 }
165 }
166 check()?;
167 Ok(graph)
168}
169
170pub(crate) fn check_parser_iteration<E>(
172 iteration: usize,
173 check: &mut impl FnMut() -> Result<(), E>,
174) -> Result<(), E> {
175 if iteration.is_multiple_of(PARSER_CONTROL_CHECK_INTERVAL) {
176 check()?;
177 }
178 Ok(())
179}
180
181#[must_use]
183pub fn has_specialized_parser(language: &str) -> bool {
184 tree_sitter_grammar(language).is_some()
185}
186
187#[must_use]
189pub fn specialized_languages() -> &'static [&'static str] {
190 builtin_tree_sitter_language_ids()
191}
192
193fn symbol_parser_owner(path: &str, language: Option<&str>) -> SymbolParserOwner {
195 if let Some(language) = language {
196 return language_capability(language).map_or(SymbolParserOwner::Fallback, |capability| {
197 capability.symbol_parser
198 });
199 }
200 let file_name = path.rsplit(['/', '\\']).next().unwrap_or(path);
201 if matches!(file_name, "Cargo.toml" | "Cargo.lock") {
202 return SymbolParserOwner::CargoManifest;
203 }
204 match Path::new(path)
205 .extension()
206 .and_then(|extension| extension.to_str())
207 {
208 Some(extension) if extension.eq_ignore_ascii_case("vue") => SymbolParserOwner::Vue,
209 Some(extension)
210 if ["ps1", "psm1", "psd1"]
211 .iter()
212 .any(|expected| extension.eq_ignore_ascii_case(expected)) =>
213 {
214 SymbolParserOwner::PowerShell
215 }
216 _ => SymbolParserOwner::Fallback,
217 }
218}
219
220fn extract_vue_sfc_graph_checked<E>(
222 path: &str,
223 language: Option<&str>,
224 content: &str,
225 check: &mut impl FnMut() -> Result<(), E>,
226) -> Result<SymbolGraph, E> {
227 let mut graph = extract_fallback_graph_checked(path, language, content, check)?;
228 graph.parser = ParserKind::Structural;
229 let mut structural = empty_graph(path, language, ParserKind::Structural);
230 for (line_index, line) in content.lines().enumerate() {
231 check_parser_iteration(line_index, check)?;
232 let trimmed = line.trim();
233 if let Some(name) = vue_composition_binding_name(trimmed) {
234 push_symbol(
235 &mut structural,
236 &name,
237 SymbolKind::Value,
238 line_index + 1,
239 line_index + 1,
240 None,
241 Some("vue-composition-binding"),
242 trimmed,
243 );
244 }
245 if is_fallback_import(trimmed) {
246 push_relation(
247 &mut structural,
248 "<module>",
249 trimmed,
250 RelationKind::Imports,
251 line_index + 1,
252 trimmed,
253 );
254 }
255 }
256 merge_preferred_graph_entries_checked(&mut graph, structural, check)?;
257 check()?;
258 Ok(graph)
259}
260
261fn extract_powershell_graph_checked<E>(
263 path: &str,
264 language: Option<&str>,
265 content: &str,
266 check: &mut impl FnMut() -> Result<(), E>,
267) -> Result<SymbolGraph, E> {
268 let mut graph = extract_fallback_graph_checked(path, language, content, check)?;
269 graph.parser = ParserKind::Structural;
270 let mut structural = empty_graph(path, language, ParserKind::Structural);
271 for (line_index, line) in content.lines().enumerate() {
272 check_parser_iteration(line_index, check)?;
273 let trimmed = line.trim();
274 if let Some(name) = powershell_function_name(trimmed) {
275 push_symbol(
276 &mut structural,
277 &name,
278 SymbolKind::Function,
279 line_index + 1,
280 line_index + 1,
281 None,
282 Some("powershell-function"),
283 trimmed,
284 );
285 }
286 if let Some(name) = powershell_class_name(trimmed) {
287 push_symbol(
288 &mut structural,
289 &name,
290 SymbolKind::Class,
291 line_index + 1,
292 line_index + 1,
293 None,
294 Some("powershell-class"),
295 trimmed,
296 );
297 }
298 if is_fallback_import(trimmed) {
299 push_relation(
300 &mut structural,
301 "<module>",
302 trimmed,
303 RelationKind::Imports,
304 line_index + 1,
305 trimmed,
306 );
307 }
308 }
309 merge_preferred_graph_entries_checked(&mut graph, structural, check)?;
310 check()?;
311 Ok(graph)
312}
313
314fn powershell_function_name(line: &str) -> Option<String> {
316 let mut parts = line.split_whitespace();
317 if !parts.next()?.eq_ignore_ascii_case("function") {
318 return None;
319 }
320 let raw_name = parts.next()?;
321 let name = raw_name.split(['(', '{']).next().unwrap_or_default().trim();
322 let name = name.rsplit_once(':').map_or(name, |(_, scoped)| scoped);
323 let valid = !name.is_empty()
324 && name
325 .chars()
326 .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '-'));
327 valid.then(|| name.to_string())
328}
329
330fn powershell_class_name(line: &str) -> Option<String> {
332 let mut parts = line.split_whitespace();
333 if !parts.next()?.eq_ignore_ascii_case("class") {
334 return None;
335 }
336 let raw_name = parts.next()?;
337 let name = raw_name
338 .split([':', '{', '('])
339 .next()
340 .unwrap_or_default()
341 .trim();
342 let valid = !name.is_empty()
343 && name
344 .chars()
345 .all(|character| character.is_ascii_alphanumeric() || character == '_');
346 valid.then(|| name.to_string())
347}
348
349fn merge_preferred_graph_entries_checked<E>(
351 graph: &mut SymbolGraph,
352 preferred: SymbolGraph,
353 check: &mut impl FnMut() -> Result<(), E>,
354) -> Result<(), E> {
355 for (iteration, symbol) in preferred.symbols.into_iter().enumerate() {
356 check_parser_iteration(iteration, check)?;
357 if let Some(existing) = graph
358 .symbols
359 .iter()
360 .position(|existing| same_symbol_identity(existing, &symbol))
361 {
362 graph.symbols[existing] = symbol;
363 } else if graph.symbols.len() < MAX_SYMBOLS_PER_FILE {
364 graph.symbols.push(symbol);
365 }
366 }
367 for (iteration, relation) in preferred.relations.into_iter().enumerate() {
368 check_parser_iteration(iteration, check)?;
369 if let Some(existing) = graph
370 .relations
371 .iter()
372 .position(|existing| same_relation_identity(existing, &relation))
373 {
374 graph.relations[existing] = relation;
375 } else if graph.relations.len() < MAX_RELATIONS_PER_FILE {
376 graph.relations.push(relation);
377 }
378 }
379 Ok(())
380}
381
382fn merge_missing_graph_entries_checked<E>(
384 graph: &mut SymbolGraph,
385 embedded: SymbolGraph,
386 host_kind: EmbeddedHostKind,
387 check: &mut impl FnMut() -> Result<(), E>,
388) -> Result<(), E> {
389 let mut symbol_identities = graph
390 .symbols
391 .iter()
392 .map(|symbol| {
393 (
394 symbol.name.clone(),
395 symbol.kind as u8,
396 symbol.line_start,
397 symbol.line_end,
398 symbol.parent.clone(),
399 )
400 })
401 .collect::<BTreeSet<_>>();
402 for (iteration, symbol) in embedded.symbols.into_iter().enumerate() {
403 check_parser_iteration(iteration, check)?;
404 if graph.symbols.len() >= MAX_SYMBOLS_PER_FILE {
405 break;
406 }
407 if host_kind == EmbeddedHostKind::Component
408 && (symbol.kind == SymbolKind::Import
409 || (symbol.kind == SymbolKind::Value && !symbol.exported))
410 {
411 continue;
412 }
413 let identity = (
414 symbol.name.clone(),
415 symbol.kind as u8,
416 symbol.line_start,
417 symbol.line_end,
418 symbol.parent.clone(),
419 );
420 if symbol_identities.insert(identity) {
421 graph.symbols.push(symbol);
422 }
423 }
424 let mut relation_identities = graph
425 .relations
426 .iter()
427 .map(|relation| {
428 (
429 relation.source_name.clone(),
430 relation.target_name.clone(),
431 relation.kind as u8,
432 relation.line,
433 )
434 })
435 .collect::<BTreeSet<_>>();
436 for (iteration, relation) in embedded.relations.into_iter().enumerate() {
437 check_parser_iteration(iteration, check)?;
438 if graph.relations.len() >= MAX_RELATIONS_PER_FILE {
439 break;
440 }
441 let identity = (
442 relation.source_name.clone(),
443 relation.target_name.clone(),
444 relation.kind as u8,
445 relation.line,
446 );
447 if relation_identities.insert(identity) {
448 graph.relations.push(relation);
449 }
450 }
451 Ok(())
452}
453
454fn same_symbol_identity(left: &CodeSymbol, right: &CodeSymbol) -> bool {
456 left.name == right.name
457 && left.kind == right.kind
458 && left.line_start == right.line_start
459 && left.line_end == right.line_end
460 && left.parent == right.parent
461}
462
463fn same_relation_identity(left: &SymbolRelation, right: &SymbolRelation) -> bool {
465 left.source_name == right.source_name
466 && left.target_name == right.target_name
467 && left.kind == right.kind
468 && left.line == right.line
469}
470
471fn vue_composition_binding_name(line: &str) -> Option<String> {
473 const MACROS: &[&str] = &[
474 "defineProps",
475 "defineEmits",
476 "defineModel",
477 "defineSlots",
478 "computed",
479 "ref",
480 "shallowRef",
481 "reactive",
482 "toRef",
483 "toRefs",
484 "watch",
485 ];
486 let rest = line
487 .strip_prefix("const ")
488 .or_else(|| line.strip_prefix("let "))
489 .or_else(|| line.strip_prefix("var "))?;
490 let (name, initializer) = rest.split_once('=')?;
491 let name = name.trim();
492 if name.is_empty() {
493 return None;
494 }
495 let initializer = initializer.trim_start();
496 MACROS
497 .iter()
498 .any(|macro_name| vue_initializer_starts_with_macro(initializer, macro_name))
499 .then(|| name.to_string())
500}
501
502fn vue_initializer_starts_with_macro(initializer: &str, macro_name: &str) -> bool {
504 vue_initializer_is_macro_call(initializer, macro_name)
505 || initializer
506 .strip_prefix("withDefaults(")
507 .is_some_and(|nested| vue_initializer_is_macro_call(nested.trim_start(), macro_name))
508}
509
510fn vue_initializer_is_macro_call(initializer: &str, macro_name: &str) -> bool {
512 let Some(rest) = initializer.strip_prefix(macro_name) else {
513 return false;
514 };
515 let rest = rest.trim_start();
516 rest.starts_with('(') || rest.starts_with('<')
517}
518
519fn extract_cargo_manifest_graph_checked<E>(
521 path: &str,
522 language: Option<&str>,
523 content: &str,
524 check: &mut impl FnMut() -> Result<(), E>,
525) -> Result<SymbolGraph, E> {
526 check()?;
527 let mut graph = empty_graph(path, language, ParserKind::Manifest);
528 let is_lock = match language {
529 Some(language) => language == "cargo-lock",
530 None => path.ends_with("Cargo.lock"),
531 };
532 if is_lock {
533 extract_cargo_lock_packages_checked(&mut graph, content, check)?;
534 check()?;
535 return Ok(graph);
536 }
537 extract_cargo_toml_entries_checked(&mut graph, content, check)?;
538 check()?;
539 Ok(graph)
540}
541
542fn extract_cargo_lock_packages_checked<E>(
544 graph: &mut SymbolGraph,
545 content: &str,
546 check: &mut impl FnMut() -> Result<(), E>,
547) -> Result<(), E> {
548 let Ok(lockfile) = content.parse::<TomlValue>() else {
549 return Ok(());
550 };
551 check()?;
552 let Some(packages) = lockfile.get("package").and_then(TomlValue::as_array) else {
553 return Ok(());
554 };
555 let mut next_package_line = 0;
556 for (iteration, package) in packages.iter().enumerate() {
557 check_parser_iteration(iteration, check)?;
558 let Some(name) = package
559 .as_table()
560 .and_then(|table| table.get("name"))
561 .and_then(TomlValue::as_str)
562 else {
563 continue;
564 };
565 let line = cargo_lock_name_line_checked(content, name, next_package_line, check)?;
566 if let Some(found_line) = line {
567 next_package_line = found_line;
568 }
569 let line = line.unwrap_or(1);
570 push_symbol(
571 graph,
572 name,
573 SymbolKind::Dependency,
574 line,
575 line,
576 None,
577 Some("cargo-lock-package"),
578 &format!("lock package {name}"),
579 );
580 }
581 Ok(())
582}
583
584fn cargo_lock_name_line_checked<E>(
586 content: &str,
587 package_name: &str,
588 start_line: usize,
589 check: &mut impl FnMut() -> Result<(), E>,
590) -> Result<Option<usize>, E> {
591 let mut in_package = false;
592 for (iteration, (index, raw_line)) in content.lines().enumerate().skip(start_line).enumerate() {
593 check_parser_iteration(iteration, check)?;
594 let line = raw_line.trim();
595 if line == "[[package]]" {
596 in_package = true;
597 continue;
598 }
599 if line.starts_with('[') {
600 in_package = false;
601 }
602 if in_package
603 && let Some((key, value)) = line.split_once('=')
604 && key.trim() == "name"
605 && value.trim().trim_matches('"') == package_name
606 {
607 return Ok(Some(index + 1));
608 }
609 }
610 Ok(None)
611}
612
613fn extract_cargo_toml_entries_checked<E>(
615 graph: &mut SymbolGraph,
616 content: &str,
617 check: &mut impl FnMut() -> Result<(), E>,
618) -> Result<(), E> {
619 let Ok(manifest) = content.parse::<TomlValue>() else {
620 return Ok(());
621 };
622 check()?;
623 let Some(root) = manifest.as_table() else {
624 return Ok(());
625 };
626 let line_index = CargoTomlLineIndex::new_checked(content, check)?;
627 if root.contains_key("workspace") {
628 let line = line_index.section_line("workspace").unwrap_or(1);
629 push_symbol(
630 graph,
631 "workspace",
632 SymbolKind::Workspace,
633 line,
634 line,
635 None,
636 Some("cargo-workspace"),
637 line_index.line_text(line).unwrap_or("[workspace]"),
638 );
639 }
640 if let Some(package) = root.get("package").and_then(TomlValue::as_table)
641 && let Some(name) = package.get("name").and_then(TomlValue::as_str)
642 {
643 let line = line_index.key_line("package", "name").unwrap_or(1);
644 push_symbol(
645 graph,
646 name,
647 SymbolKind::Package,
648 line,
649 line,
650 None,
651 Some("cargo-package"),
652 line_index.line_text(line).unwrap_or(name),
653 );
654 }
655 collect_cargo_dependencies_checked(graph, &line_index, &[], root, check)?;
656 Ok(())
657}
658
659fn collect_cargo_dependencies_checked<E>(
661 graph: &mut SymbolGraph,
662 line_index: &CargoTomlLineIndex,
663 path: &[String],
664 table: &toml::map::Map<String, TomlValue>,
665 check: &mut impl FnMut() -> Result<(), E>,
666) -> Result<(), E> {
667 check()?;
668 let section = path.join(".");
669 if is_dependency_table_path(path) {
670 for (iteration, (name, value)) in table.iter().enumerate() {
671 check_parser_iteration(iteration, check)?;
672 let line = line_index
673 .key_line(§ion, name)
674 .or_else(|| line_index.section_line(§ion))
675 .unwrap_or(1);
676 let detail = line_index
677 .line_text(line)
678 .map_or_else(|| name.as_str(), str::trim);
679 let dependency_name = manifest_dependency_name(name, value);
680 push_symbol(
681 graph,
682 &dependency_name,
683 SymbolKind::Dependency,
684 line,
685 line,
686 Some(section.clone()),
687 Some("cargo-dependency"),
688 detail,
689 );
690 push_relation(
691 graph,
692 "cargo",
693 &dependency_name,
694 RelationKind::DependsOn,
695 line,
696 detail,
697 );
698 }
699 return Ok(());
700 }
701 for (iteration, (key, value)) in table.iter().enumerate() {
702 check_parser_iteration(iteration, check)?;
703 let Some(child) = value.as_table() else {
704 continue;
705 };
706 let mut child_path = path.to_owned();
707 child_path.push(key.clone());
708 collect_cargo_dependencies_checked(graph, line_index, &child_path, child, check)?;
709 }
710 Ok(())
711}
712
713fn is_dependency_table_path(path: &[String]) -> bool {
715 path.last().is_some_and(|last| {
716 last == "dependencies" || last == "dev-dependencies" || last == "build-dependencies"
717 })
718}
719
720fn manifest_dependency_name(key: &str, value: &TomlValue) -> String {
722 value
723 .as_table()
724 .and_then(|table| table.get("package"))
725 .and_then(TomlValue::as_str)
726 .unwrap_or(key)
727 .to_string()
728}
729
730struct CargoTomlLineIndex<'a> {
732 lines: Vec<&'a str>,
734 sections: std::collections::HashMap<String, usize>,
736 keys: std::collections::HashMap<(String, String), usize>,
738}
739
740impl<'a> CargoTomlLineIndex<'a> {
741 fn new_checked<E>(
743 content: &'a str,
744 check: &mut impl FnMut() -> Result<(), E>,
745 ) -> Result<Self, E> {
746 let lines = content.lines().collect::<Vec<_>>();
747 let mut sections = std::collections::HashMap::new();
748 let mut keys = std::collections::HashMap::new();
749 let mut current_section = String::new();
750 for (index, raw_line) in lines.iter().enumerate() {
751 check_parser_iteration(index, check)?;
752 let line_number = index + 1;
753 let line = raw_line.trim();
754 if line.starts_with('[') && line.ends_with(']') {
755 current_section = normalize_toml_section(line.trim_matches(&['[', ']'][..]).trim());
756 sections.insert(current_section.clone(), line_number);
757 continue;
758 }
759 let Some((key, _value)) = line.split_once('=') else {
760 continue;
761 };
762 let key = key.trim().trim_matches('"').to_string();
763 if !key.is_empty() {
764 keys.insert((current_section.clone(), key), line_number);
765 }
766 }
767 Ok(Self {
768 lines,
769 sections,
770 keys,
771 })
772 }
773
774 fn section_line(&self, section: &str) -> Option<usize> {
776 self.sections.get(section).copied()
777 }
778
779 fn key_line(&self, section: &str, key: &str) -> Option<usize> {
781 self.keys
782 .get(&(section.to_string(), key.to_string()))
783 .copied()
784 }
785
786 fn line_text(&self, line: usize) -> Option<&'a str> {
788 self.lines.get(line.checked_sub(1)?).copied()
789 }
790}
791
792fn normalize_toml_section(section: &str) -> String {
794 let mut parts = Vec::new();
795 let mut current = String::new();
796 let mut quote: Option<char> = None;
797 for character in section.chars() {
798 match (character, quote) {
799 ('"' | '\'', None) => quote = Some(character),
800 (value, Some(active)) if value == active => quote = None,
801 ('.', None) => {
802 if !current.is_empty() {
803 parts.push(current.clone());
804 current.clear();
805 }
806 }
807 (value, _) => current.push(value),
808 }
809 }
810 if !current.is_empty() {
811 parts.push(current);
812 }
813 parts.join(".")
814}
815
816struct TreeSitterParse {
818 graph: SymbolGraph,
820 had_errors: bool,
822}
823
824fn extract_tree_sitter_graph<E>(
826 path: &str,
827 language: Option<&str>,
828 content: &str,
829 check: &mut impl FnMut() -> Result<(), E>,
830) -> Result<Option<TreeSitterParse>, E> {
831 let Some(language_name) = language else {
832 return Ok(None);
833 };
834 let Some(parser_language) = tree_sitter_language(language_name) else {
835 return Ok(None);
836 };
837 check()?;
838 let mut parser = Parser::new();
839 if parser.set_language(&parser_language).is_err() {
840 return Ok(None);
841 }
842 let mut parse_failure = None;
843 let mut progress = |_: &tree_sitter::ParseState| match check() {
844 Ok(()) => ControlFlow::Continue(()),
845 Err(error) => {
846 parse_failure = Some(error);
847 ControlFlow::Break(())
848 }
849 };
850 let bytes = content.as_bytes();
851 let mut read = |offset, _| bytes.get(offset..).unwrap_or_default();
852 let tree = parser.parse_with_options(
853 &mut read,
854 None,
855 Some(ParseOptions::new().progress_callback(&mut progress)),
856 );
857 if let Some(error) = parse_failure {
858 return Err(error);
859 }
860 check()?;
861 let Some(tree) = tree else {
862 return Ok(None);
863 };
864 let mut graph = empty_graph(path, language, ParserKind::TreeSitter);
865 let root = tree.root_node();
866 let had_errors = root.has_error();
867 visit_node(root, content, &mut graph, check)?;
868 check()?;
869 languages::augment_language_graph(&mut graph, content, check)?;
870 check()?;
871 Ok(Some(TreeSitterParse { graph, had_errors }))
872}
873
874fn tree_sitter_language(language: &str) -> Option<Language> {
876 Some(match tree_sitter_grammar(language)? {
877 TreeSitterGrammar::Rust => tree_sitter_rust::LANGUAGE.into(),
878 TreeSitterGrammar::Python => tree_sitter_python::LANGUAGE.into(),
879 TreeSitterGrammar::JavaScript => tree_sitter_javascript::LANGUAGE.into(),
880 TreeSitterGrammar::TypeScript => tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(),
881 TreeSitterGrammar::Tsx => tree_sitter_typescript::LANGUAGE_TSX.into(),
882 TreeSitterGrammar::Java => tree_sitter_java::LANGUAGE.into(),
883 TreeSitterGrammar::Kotlin => tree_sitter_kotlin_ng::LANGUAGE.into(),
884 TreeSitterGrammar::CSharp => tree_sitter_c_sharp::LANGUAGE.into(),
885 TreeSitterGrammar::Go => tree_sitter_go::LANGUAGE.into(),
886 TreeSitterGrammar::ObjectiveC => tree_sitter_objc::LANGUAGE.into(),
887 TreeSitterGrammar::Zig => tree_sitter_zig::LANGUAGE.into(),
888 TreeSitterGrammar::C => tree_sitter_c::LANGUAGE.into(),
889 TreeSitterGrammar::Cpp => tree_sitter_cpp::LANGUAGE.into(),
890 })
891}
892
893fn visit_node<E>(
895 node: Node<'_>,
896 content: &str,
897 graph: &mut SymbolGraph,
898 check: &mut impl FnMut() -> Result<(), E>,
899) -> Result<(), E> {
900 check()?;
901 if graph.symbols.len() < MAX_SYMBOLS_PER_FILE
902 && let Some(kind) = declaration_kind(node.kind())
903 && should_emit_declaration_symbol(node, content)
904 {
905 push_tree_symbol(graph, node, content, effective_declaration_kind(node, kind));
906 }
907 if graph.relations.len() < MAX_RELATIONS_PER_FILE {
908 if is_import_node(node.kind()) {
909 push_import_relation(graph, node, content);
910 } else if is_call_node(node.kind()) {
911 push_call_relation(graph, node, content);
912 }
913 }
914 let mut cursor = node.walk();
915 for child in node.named_children(&mut cursor) {
916 visit_node(child, content, graph, check)?;
917 }
918 Ok(())
919}
920
921fn effective_declaration_kind(node: Node<'_>, kind: SymbolKind) -> SymbolKind {
923 if kind == SymbolKind::Function && declaration_is_method_context(node) {
924 return SymbolKind::Method;
925 }
926 if kind == SymbolKind::Value
927 && !is_local_value_declaration(node)
928 && declaration_has_direct_callable_initializer(node)
929 {
930 return SymbolKind::Function;
931 }
932 if kind == SymbolKind::Type {
933 if has_descendant_kind(node, &["struct_type"]) {
934 return SymbolKind::Struct;
935 }
936 if has_descendant_kind(node, &["interface_type"]) {
937 return SymbolKind::Interface;
938 }
939 }
940 kind
941}
942
943fn declaration_is_method_context(node: Node<'_>) -> bool {
945 matches!(
946 node.kind(),
947 "function_item" | "function_definition" | "function_declaration" | "function_declarator"
948 ) && (has_ancestor_kind(node.parent(), "impl_item")
949 || has_ancestor_kind(node.parent(), "class_definition")
950 || has_ancestor_kind(node.parent(), "class_declaration")
951 || has_ancestor_kind(node.parent(), "class_body")
952 || has_ancestor_kind(node.parent(), "class_specifier")
953 || has_ancestor_kind(node.parent(), "struct_specifier")
954 || has_ancestor_kind(node.parent(), "interface_declaration"))
955}
956
957fn should_emit_declaration_symbol(node: Node<'_>, content: &str) -> bool {
959 if is_object_literal_method(node) {
960 return object_literal_method_owner(node, content).is_some_and(|owner| owner.exported);
961 }
962 if node.kind() == "field_declaration"
963 && has_descendant_kind(node, &["function_declarator", "method_declarator"])
964 {
965 return false;
966 }
967 if matches!(node.kind(), "function_declarator" | "method_declarator") {
968 if is_type_member_declarator(node) {
969 return true;
970 }
971 return !has_declaration_ancestor(node.parent());
972 }
973 true
974}
975
976fn is_type_member_declarator(node: Node<'_>) -> bool {
978 has_ancestor_kind(node.parent(), "field_declaration")
979 && (has_ancestor_kind(node.parent(), "class_specifier")
980 || has_ancestor_kind(node.parent(), "struct_specifier"))
981 && !has_ancestor_kind(node.parent(), "function_definition")
982}
983
984fn has_declaration_ancestor(mut node: Option<Node<'_>>) -> bool {
986 while let Some(current) = node {
987 if declaration_kind(current.kind()).is_some() {
988 return true;
989 }
990 node = current.parent();
991 }
992 false
993}
994
995fn declaration_has_direct_callable_initializer(node: Node<'_>) -> bool {
997 if !matches!(
998 node.kind(),
999 "lexical_declaration" | "variable_declaration" | "variable_statement" | "var_declaration"
1000 ) {
1001 return false;
1002 }
1003 first_declaration_initializer(node).is_some_and(|initializer| {
1004 matches!(
1005 initializer.kind(),
1006 "arrow_function"
1007 | "function"
1008 | "function_expression"
1009 | "generator_function"
1010 | "lambda_expression"
1011 )
1012 })
1013}
1014
1015fn first_declaration_initializer(node: Node<'_>) -> Option<Node<'_>> {
1017 if let Some(value) = node.child_by_field_name("value") {
1018 return Some(value);
1019 }
1020 let mut cursor = node.walk();
1021 for child in node.named_children(&mut cursor) {
1022 if let Some(value) = child.child_by_field_name("value") {
1023 return Some(value);
1024 }
1025 }
1026 None
1027}
1028
1029fn is_local_value_declaration(node: Node<'_>) -> bool {
1031 matches!(
1032 node.kind(),
1033 "lexical_declaration" | "variable_declaration" | "variable_statement" | "var_declaration"
1034 ) && has_ancestor_kind_any(
1035 node.parent(),
1036 &[
1037 "arrow_function",
1038 "function",
1039 "function_expression",
1040 "function_declaration",
1041 "generator_function",
1042 "method_definition",
1043 "method_declaration",
1044 "function_item",
1045 "function_definition",
1046 "function_declaration_with_receiver",
1047 "func_literal",
1048 ],
1049 )
1050}
1051
1052fn is_object_literal_method(node: Node<'_>) -> bool {
1054 node.kind() == "method_definition"
1055 && has_ancestor_kind_any(node.parent(), &["object", "object_pattern", "pair"])
1056}
1057
1058#[derive(Clone, Debug, Eq, PartialEq)]
1060struct ObjectLiteralMethodOwner {
1061 name: String,
1063 exported: bool,
1065}
1066
1067fn object_literal_method_owner(
1069 method_node: Node<'_>,
1070 content: &str,
1071) -> Option<ObjectLiteralMethodOwner> {
1072 if !is_object_literal_method(method_node) {
1073 return None;
1074 }
1075 let object = nearest_ancestor_kind(method_node.parent(), "object")?;
1076 object_literal_owner(object, content)
1077}
1078
1079fn object_literal_owner(object: Node<'_>, content: &str) -> Option<ObjectLiteralMethodOwner> {
1081 let parent = object.parent()?;
1082 match parent.kind() {
1083 "variable_declarator" | "variable_declaration" => {
1084 let name = declarator_name(parent, content)?;
1085 Some(ObjectLiteralMethodOwner {
1086 name,
1087 exported: is_directly_exported_declaration(parent),
1088 })
1089 }
1090 "assignment_expression" | "augmented_assignment_expression" => {
1091 let target = parent
1092 .child_by_field_name("left")
1093 .or_else(|| first_named_child(parent))?;
1094 let name = compact_text(node_text(target, content).as_deref().unwrap_or(""));
1095 if name.is_empty() {
1096 return None;
1097 }
1098 let exported = name == "module.exports"
1099 || name.starts_with("module.exports.")
1100 || name == "exports"
1101 || name.starts_with("exports.");
1102 Some(ObjectLiteralMethodOwner { name, exported })
1103 }
1104 "export_statement" => Some(ObjectLiteralMethodOwner {
1105 name: "default".to_string(),
1106 exported: true,
1107 }),
1108 "pair" => {
1109 let property = parent
1110 .child_by_field_name("key")
1111 .and_then(|key| named_text(key, content))
1112 .unwrap_or_else(|| "object".to_string());
1113 let outer = nearest_ancestor_kind(parent.parent(), "object")
1114 .and_then(|outer| object_literal_owner(outer, content));
1115 outer.map(|owner| ObjectLiteralMethodOwner {
1116 name: format!("{}.{}", owner.name, property),
1117 exported: owner.exported,
1118 })
1119 }
1120 _ => None,
1121 }
1122}
1123
1124fn is_directly_exported_declaration(node: Node<'_>) -> bool {
1126 let mut current = Some(node);
1127 while let Some(candidate) = current {
1128 if has_direct_export_parent(candidate) {
1129 return true;
1130 }
1131 if matches!(
1132 candidate.kind(),
1133 "lexical_declaration" | "variable_declaration" | "variable_statement"
1134 ) {
1135 return false;
1136 }
1137 current = candidate.parent();
1138 }
1139 false
1140}
1141
1142fn has_ancestor_kind(mut node: Option<Node<'_>>, kind: &str) -> bool {
1144 while let Some(current) = node {
1145 if current.kind() == kind {
1146 return true;
1147 }
1148 node = current.parent();
1149 }
1150 false
1151}
1152
1153fn has_ancestor_kind_any(mut node: Option<Node<'_>>, kinds: &[&str]) -> bool {
1155 while let Some(current) = node {
1156 if kinds.contains(¤t.kind()) {
1157 return true;
1158 }
1159 node = current.parent();
1160 }
1161 false
1162}
1163
1164fn nearest_ancestor_kind<'tree>(mut node: Option<Node<'tree>>, kind: &str) -> Option<Node<'tree>> {
1166 while let Some(current) = node {
1167 if current.kind() == kind {
1168 return Some(current);
1169 }
1170 node = current.parent();
1171 }
1172 None
1173}
1174
1175fn push_tree_symbol(
1177 graph: &mut SymbolGraph,
1178 node: Node<'_>,
1179 content: &str,
1180 symbol_kind: SymbolKind,
1181) {
1182 let name = node_name(node, content)
1183 .unwrap_or_else(|| compact_text(node_text(node, content).as_deref().unwrap_or("")));
1184 if name.is_empty() {
1185 return;
1186 }
1187 let signature = declaration_signature(node, content);
1188 let parent = symbol_parent(node, content);
1189 let exported = has_direct_export_parent(node)
1190 || object_literal_method_owner(node, content).is_some_and(|owner| owner.exported)
1191 || is_exported_symbol(graph.language.as_deref(), &name, &signature);
1192 let documentation = symbol_documentation(node, content);
1193 push_symbol_with_metadata(
1194 graph,
1195 &name,
1196 symbol_kind,
1197 node.start_position().row + 1,
1198 node.end_position().row + 1,
1199 parent.clone(),
1200 Some(node.kind()),
1201 &signature,
1202 exported,
1203 documentation.as_deref(),
1204 );
1205 if let Some(parent_name) = parent {
1206 push_relation(
1207 graph,
1208 &parent_name,
1209 &name,
1210 RelationKind::Contains,
1211 node.start_position().row + 1,
1212 node.kind(),
1213 );
1214 }
1215}
1216
1217fn has_direct_export_parent(node: Node<'_>) -> bool {
1219 node.parent()
1220 .is_some_and(|parent| parent.kind() == "export_statement")
1221}
1222
1223fn content_without_leading_purpose_header(content: &str) -> Cow<'_, str> {
1225 let Some(start) = content.find(|character: char| !character.is_whitespace()) else {
1226 return Cow::Borrowed(content);
1227 };
1228 let rest = &content[start..];
1229 if let Some(end) = leading_purpose_block_end(rest) {
1230 return Cow::Owned(blank_prefix_preserving_newlines(content, start + end));
1231 }
1232 if let Some(end) = leading_purpose_line_end(rest) {
1233 return Cow::Owned(blank_prefix_preserving_newlines(content, start + end));
1234 }
1235 Cow::Borrowed(content)
1236}
1237
1238fn leading_purpose_block_end(rest: &str) -> Option<usize> {
1240 if !(rest.starts_with("/**") || rest.starts_with("/*")) {
1241 return None;
1242 }
1243 let end = rest.find("*/")? + "*/".len();
1244 let documentation = rest[..end]
1245 .lines()
1246 .filter_map(|line| clean_doc_comment_line(line.trim()))
1247 .collect::<Vec<_>>()
1248 .join(" ");
1249 compact_documentation(&documentation)
1250 .is_some_and(|value| value.starts_with("Purpose:"))
1251 .then_some(end)
1252}
1253
1254fn leading_purpose_line_end(rest: &str) -> Option<usize> {
1256 let line_end = rest.find('\n').map_or(rest.len(), |index| index + 1);
1257 let line = rest[..line_end].trim();
1258 let cleaned = line
1259 .strip_prefix("//")
1260 .or_else(|| line.strip_prefix('#'))
1261 .or_else(|| {
1262 line.strip_prefix("<!--")
1263 .and_then(|value| value.strip_suffix("-->"))
1264 })?
1265 .trim();
1266 cleaned.starts_with("Purpose:").then_some(line_end)
1267}
1268
1269fn blank_prefix_preserving_newlines(content: &str, end: usize) -> String {
1271 let mut output = String::with_capacity(content.len());
1272 debug_assert!(content.is_char_boundary(end));
1273 for byte in &content.as_bytes()[..end] {
1274 output.push(if matches!(*byte, b'\n' | b'\r') {
1275 char::from(*byte)
1276 } else {
1277 ' '
1278 });
1279 }
1280 output.push_str(&content[end..]);
1281 output
1282}
1283
1284fn symbol_parent(node: Node<'_>, content: &str) -> Option<String> {
1286 if let Some(owner) = object_literal_method_owner(node, content) {
1287 return Some(owner.name);
1288 }
1289 if node.kind() == "function_item"
1290 && let Some(impl_node) = nearest_ancestor_kind(node.parent(), "impl_item")
1291 {
1292 return impl_type_name(impl_node, content);
1293 }
1294 if matches!(node.kind(), "function_declarator" | "method_declarator")
1295 && let Some(type_node) = nearest_ancestor_kind(node.parent(), "class_specifier")
1296 .or_else(|| nearest_ancestor_kind(node.parent(), "struct_specifier"))
1297 {
1298 return node_name(type_node, content);
1299 }
1300 enclosing_symbol_name(node.parent(), content)
1301}
1302
1303fn declaration_kind(kind: &str) -> Option<SymbolKind> {
1305 match kind {
1306 "function_item"
1307 | "function_declaration"
1308 | "function_definition"
1309 | "function_declarator"
1310 | "func_literal" => Some(SymbolKind::Function),
1311 "method_definition"
1312 | "method_declarator"
1313 | "method_declaration"
1314 | "function_declaration_with_receiver"
1315 | "constructor_declaration"
1316 | "init_declaration" => Some(SymbolKind::Method),
1317 "class_declaration"
1318 | "class_definition"
1319 | "class_specifier"
1320 | "class_interface"
1321 | "class_implementation" => Some(SymbolKind::Class),
1322 "struct_item" | "struct_specifier" | "struct_declaration" => Some(SymbolKind::Struct),
1323 "enum_item" | "enum_declaration" | "enum_specifier" => Some(SymbolKind::Enum),
1324 "trait_item" => Some(SymbolKind::Trait),
1325 "interface_declaration" | "interface_type" => Some(SymbolKind::Interface),
1326 "mod_item"
1327 | "module_declaration"
1328 | "namespace_declaration"
1329 | "file_scoped_namespace_declaration"
1330 | "package_declaration"
1331 | "package_clause"
1332 | "package_header" => Some(SymbolKind::Module),
1333 "type_item" | "type_alias_declaration" | "type_declaration" => Some(SymbolKind::Type),
1334 "const_item"
1335 | "static_item"
1336 | "const_declaration"
1337 | "field_declaration"
1338 | "lexical_declaration"
1339 | "var_declaration"
1340 | "short_var_declaration" => Some(SymbolKind::Value),
1341 "use_declaration"
1342 | "import_statement"
1343 | "import_declaration"
1344 | "import_from_statement"
1345 | "using_directive"
1346 | "preproc_include" => Some(SymbolKind::Import),
1347 _ => None,
1348 }
1349}
1350
1351fn is_import_node(kind: &str) -> bool {
1353 matches!(
1354 kind,
1355 "use_declaration"
1356 | "import_statement"
1357 | "import_declaration"
1358 | "import_from_statement"
1359 | "using_directive"
1360 | "preproc_include"
1361 )
1362}
1363
1364fn is_call_node(kind: &str) -> bool {
1366 matches!(
1367 kind,
1368 "call_expression"
1369 | "method_invocation"
1370 | "invocation_expression"
1371 | "call"
1372 | "macro_invocation"
1373 )
1374}
1375
1376fn has_descendant_kind(node: Node<'_>, kinds: &[&str]) -> bool {
1378 let mut cursor = node.walk();
1379 for child in node.named_children(&mut cursor) {
1380 if kinds.contains(&child.kind()) || has_descendant_kind(child, kinds) {
1381 return true;
1382 }
1383 }
1384 false
1385}
1386
1387fn push_import_relation(graph: &mut SymbolGraph, node: Node<'_>, content: &str) {
1389 let import_text = compact_text(node_text(node, content).as_deref().unwrap_or(""));
1390 if import_text.is_empty() {
1391 return;
1392 }
1393 push_relation(
1394 graph,
1395 "<module>",
1396 &import_text,
1397 RelationKind::Imports,
1398 node.start_position().row + 1,
1399 &import_text,
1400 );
1401}
1402
1403fn push_call_relation(graph: &mut SymbolGraph, node: Node<'_>, content: &str) {
1405 let target_node = node
1406 .child_by_field_name("function")
1407 .or_else(|| first_named_child(node));
1408 let Some(target_node) = target_node else {
1409 return;
1410 };
1411 let target = compact_text(node_text(target_node, content).as_deref().unwrap_or(""));
1412 if target.is_empty() || target.len() > MAX_SNIPPET_CHARS {
1413 return;
1414 }
1415 let source = enclosing_symbol_name(node.parent(), content).unwrap_or_else(|| "<module>".into());
1416 let context = compact_text(node_text(node, content).as_deref().unwrap_or(""));
1417 push_relation(
1418 graph,
1419 &source,
1420 &target,
1421 RelationKind::Calls,
1422 node.start_position().row + 1,
1423 &context,
1424 );
1425 if graph.language.as_deref() == Some("rust")
1426 && rust_target_invokes_function_item(target_node, content)
1427 && let Some(arguments) = node.child_by_field_name("arguments")
1428 && let Some(callback) = first_named_child(arguments)
1429 && callback.kind() == "scoped_identifier"
1430 {
1431 let callback = compact_text(node_text(callback, content).as_deref().unwrap_or(""));
1432 if !callback.is_empty() && callback.len() <= MAX_SNIPPET_CHARS {
1433 push_relation(
1434 graph,
1435 &source,
1436 &callback,
1437 RelationKind::Calls,
1438 node.start_position().row + 1,
1439 &context,
1440 );
1441 }
1442 }
1443}
1444
1445fn rust_target_invokes_function_item(target: Node<'_>, content: &str) -> bool {
1447 if target.kind() != "field_expression"
1448 || target
1449 .child_by_field_name("field")
1450 .and_then(|field| node_text(field, content))
1451 .as_deref()
1452 != Some("then")
1453 {
1454 return false;
1455 }
1456 target
1457 .child_by_field_name("value")
1458 .is_some_and(|receiver| rust_expression_is_definitely_bool(receiver, content))
1459}
1460
1461fn rust_expression_is_definitely_bool(mut expression: Node<'_>, content: &str) -> bool {
1463 while expression.kind() == "parenthesized_expression" {
1464 let Some(inner) = first_named_child(expression) else {
1465 return false;
1466 };
1467 expression = inner;
1468 }
1469 match expression.kind() {
1470 "boolean_literal" => true,
1471 "binary_expression" => {
1472 let (Some(left), Some(right)) = (
1473 expression.child_by_field_name("left"),
1474 expression.child_by_field_name("right"),
1475 ) else {
1476 return false;
1477 };
1478 content
1479 .get(left.end_byte()..right.start_byte())
1480 .is_some_and(|operator| {
1481 matches!(
1482 operator.trim(),
1483 "==" | "!=" | "<" | "<=" | ">" | ">=" | "&&" | "||"
1484 )
1485 })
1486 }
1487 _ => false,
1488 }
1489}
1490
1491fn first_named_child(node: Node<'_>) -> Option<Node<'_>> {
1493 let mut cursor = node.walk();
1494 node.named_children(&mut cursor).next()
1495}
1496
1497fn node_name(node: Node<'_>, content: &str) -> Option<String> {
1499 if let Some(name) = declaration_specific_name(node, content) {
1500 return Some(name);
1501 }
1502 if let Some(declarator) = node.child_by_field_name("declarator")
1503 && let Some(name) = declarator_name(declarator, content)
1504 {
1505 return Some(name);
1506 }
1507 for field_name in ["name", "field", "property", "type", "path"] {
1508 if let Some(child) = node.child_by_field_name(field_name)
1509 && let Some(name) = named_text(child, content)
1510 {
1511 return Some(name);
1512 }
1513 }
1514 let mut cursor = node.walk();
1515 for child in node.named_children(&mut cursor) {
1516 if matches!(
1517 child.kind(),
1518 "identifier" | "type_identifier" | "property_identifier" | "field_identifier"
1519 ) && let Some(name) = named_text(child, content)
1520 {
1521 return Some(name);
1522 }
1523 }
1524 None
1525}
1526
1527fn declaration_specific_name(node: Node<'_>, content: &str) -> Option<String> {
1529 match node.kind() {
1530 "package_declaration" | "package_clause" | "package_header" => {
1531 prefixed_declaration_name(node, content, &["package"])
1532 }
1533 "namespace_declaration" | "file_scoped_namespace_declaration" => {
1534 prefixed_declaration_name(node, content, &["namespace"])
1535 }
1536 "module_declaration" => {
1537 prefixed_declaration_name(node, content, &["module", "declare module"])
1538 }
1539 "type_declaration" => keyword_identifier_name(node, content, "type"),
1540 "lexical_declaration"
1541 | "variable_declaration"
1542 | "variable_statement"
1543 | "var_declaration" => first_variable_declarator_name(node, content),
1544 _ => None,
1545 }
1546}
1547
1548fn prefixed_declaration_name(node: Node<'_>, content: &str, prefixes: &[&str]) -> Option<String> {
1550 let text = compact_text(&node_text(node, content)?);
1551 for prefix in prefixes {
1552 let Some(rest) = text.strip_prefix(prefix) else {
1553 continue;
1554 };
1555 let name = rest
1556 .trim()
1557 .trim_matches('"')
1558 .trim_end_matches(';')
1559 .trim_end_matches('{')
1560 .trim()
1561 .to_string();
1562 if !name.is_empty() {
1563 return Some(name);
1564 }
1565 }
1566 None
1567}
1568
1569fn keyword_identifier_name(node: Node<'_>, content: &str, keyword: &str) -> Option<String> {
1571 let text = compact_text(&node_text(node, content)?);
1572 let rest = text.strip_prefix(keyword)?.trim();
1573 rest.split_whitespace()
1574 .next()
1575 .map(|name| name.trim_matches(';').to_string())
1576 .filter(|name| !name.is_empty())
1577}
1578
1579fn impl_type_name(node: Node<'_>, content: &str) -> Option<String> {
1581 if let Some(type_node) = node.child_by_field_name("type")
1582 && let Some(name) = named_text(type_node, content)
1583 {
1584 return Some(clean_type_name(&name));
1585 }
1586 let mut cursor = node.walk();
1587 for child in node.named_children(&mut cursor) {
1588 if matches!(
1589 child.kind(),
1590 "type_identifier" | "scoped_type_identifier" | "generic_type" | "identifier"
1591 ) && let Some(name) = named_text(child, content)
1592 {
1593 return Some(clean_type_name(&name));
1594 }
1595 }
1596 None
1597}
1598
1599fn clean_type_name(value: &str) -> String {
1601 value
1602 .trim()
1603 .trim_start_matches('&')
1604 .trim_start_matches("mut ")
1605 .split(['<', ' ', '{'])
1606 .next()
1607 .unwrap_or(value)
1608 .trim()
1609 .to_string()
1610}
1611
1612fn first_variable_declarator_name(node: Node<'_>, content: &str) -> Option<String> {
1614 let mut cursor = node.walk();
1615 for child in node.named_children(&mut cursor) {
1616 if matches!(
1617 child.kind(),
1618 "variable_declarator" | "variable_declaration" | "identifier"
1619 ) && let Some(name) = declarator_name(child, content)
1620 {
1621 return Some(name);
1622 }
1623 }
1624 None
1625}
1626
1627fn declarator_name(node: Node<'_>, content: &str) -> Option<String> {
1629 if let Some(name_node) = node.child_by_field_name("name")
1630 && let Some(name) = named_text(name_node, content)
1631 {
1632 return Some(strip_declarator_noise(&name));
1633 }
1634 if matches!(
1635 node.kind(),
1636 "identifier" | "type_identifier" | "property_identifier" | "field_identifier"
1637 ) && let Some(name) = named_text(node, content)
1638 {
1639 return Some(strip_declarator_noise(&name));
1640 }
1641 let mut cursor = node.walk();
1642 for child in node.named_children(&mut cursor) {
1643 if let Some(name) = declarator_name(child, content) {
1644 return Some(name);
1645 }
1646 }
1647 None
1648}
1649
1650fn strip_declarator_noise(value: &str) -> String {
1652 value
1653 .split(['=', '(', ':'])
1654 .next()
1655 .unwrap_or(value)
1656 .trim()
1657 .to_string()
1658}
1659
1660fn named_text(node: Node<'_>, content: &str) -> Option<String> {
1662 let text = node_text(node, content)?;
1663 let compact = compact_text(&text);
1664 if compact.is_empty() {
1665 None
1666 } else {
1667 Some(compact)
1668 }
1669}
1670
1671fn declaration_signature(node: Node<'_>, content: &str) -> String {
1673 let header_end = declaration_body_start(node).unwrap_or_else(|| node.end_byte());
1674 let mut signature = String::new();
1675 append_declaration_tokens(node, content, header_end, &mut signature);
1676 if signature.is_empty() {
1677 node_text(node, content).map_or_else(String::new, |raw| compact_text(&raw))
1678 } else {
1679 signature
1680 }
1681}
1682
1683fn declaration_body_start(node: Node<'_>) -> Option<usize> {
1685 if declaration_has_direct_callable_initializer(node)
1686 && let Some(initializer) = first_declaration_initializer(node)
1687 && let Some(body) = initializer.child_by_field_name("body")
1688 {
1689 return Some(body.start_byte());
1690 }
1691 if declaration_kind(node.kind()) == Some(SymbolKind::Value)
1692 && let Some(initializer) = first_declaration_initializer(node)
1693 {
1694 return Some(initializer.start_byte());
1695 }
1696 if let Some(body) = node.child_by_field_name("body") {
1697 return Some(body.start_byte());
1698 }
1699 let mut cursor = node.walk();
1700 node.named_children(&mut cursor)
1701 .find(|child| {
1702 matches!(
1703 child.kind(),
1704 "block" | "compound_statement" | "statement_block"
1705 ) || child.kind().ends_with("_body")
1706 })
1707 .map(|body| body.start_byte())
1708}
1709
1710fn append_declaration_tokens(
1712 node: Node<'_>,
1713 content: &str,
1714 header_end: usize,
1715 signature: &mut String,
1716) {
1717 if node.start_byte() >= header_end || node.kind().contains("comment") {
1718 return;
1719 }
1720 if node.child_count() == 0 {
1721 if node.end_byte() <= header_end
1722 && let Ok(token) = node.utf8_text(content.as_bytes())
1723 {
1724 let token = token.trim();
1725 if !token.is_empty() {
1726 if !signature.is_empty() {
1727 signature.push(' ');
1728 }
1729 signature.push_str(token);
1730 }
1731 }
1732 return;
1733 }
1734 let mut cursor = node.walk();
1735 for child in node.children(&mut cursor) {
1736 append_declaration_tokens(child, content, header_end, signature);
1737 }
1738}
1739
1740fn is_exported_symbol(language: Option<&str>, name: &str, signature: &str) -> bool {
1742 let trimmed = signature.trim_start();
1743 trimmed.starts_with("pub ")
1744 || trimmed.starts_with("pub(")
1745 || trimmed.starts_with("export ")
1746 || trimmed.starts_with("public ")
1747 || trimmed.starts_with("open ")
1748 || matches!(language, Some("go")) && starts_with_uppercase(name)
1749}
1750
1751fn starts_with_uppercase(value: &str) -> bool {
1753 value.chars().next().is_some_and(char::is_uppercase)
1754}
1755
1756fn symbol_documentation(node: Node<'_>, content: &str) -> Option<String> {
1758 preceding_documentation(content, node.start_position().row + 1)
1759 .or_else(|| leading_docstring_literal(node, content))
1760}
1761
1762fn preceding_documentation(content: &str, line_start: usize) -> Option<String> {
1764 let lines = content.lines().collect::<Vec<_>>();
1765 if line_start <= 1 || lines.is_empty() {
1766 return None;
1767 }
1768 let mut index = line_start.saturating_sub(2);
1769 let mut collected = Vec::new();
1770 let mut saw_doc = false;
1771 loop {
1772 let trimmed = lines[index].trim();
1773 if trimmed.is_empty() {
1774 break;
1775 }
1776 if !saw_doc && is_attribute_line(trimmed) {
1777 if index == 0 {
1778 break;
1779 }
1780 index -= 1;
1781 continue;
1782 }
1783 if let Some(line) = clean_doc_comment_line(trimmed) {
1784 collected.push(line);
1785 saw_doc = true;
1786 if index == 0 {
1787 break;
1788 }
1789 index -= 1;
1790 continue;
1791 }
1792 break;
1793 }
1794 collected.reverse();
1795 compact_documentation(&collected.join(" "))
1796}
1797
1798fn is_attribute_line(trimmed: &str) -> bool {
1800 trimmed.starts_with("#[") || trimmed.starts_with('@')
1801}
1802
1803fn clean_doc_comment_line(trimmed: &str) -> Option<String> {
1805 let cleaned = if let Some(rest) = trimmed.strip_prefix("///") {
1806 rest
1807 } else if let Some(rest) = trimmed.strip_prefix("/**") {
1808 rest
1809 } else if let Some(rest) = trimmed.strip_prefix("*/") {
1810 rest
1811 } else if let Some(rest) = trimmed.strip_prefix('*') {
1812 rest
1813 } else if let Some(rest) = trimmed.strip_prefix("# ") {
1814 rest
1815 } else {
1816 trimmed.strip_prefix("## ")?
1817 }
1818 .trim()
1819 .trim_end_matches("*/")
1820 .trim()
1821 .to_string();
1822 Some(cleaned)
1823}
1824
1825fn leading_docstring_literal(node: Node<'_>, content: &str) -> Option<String> {
1827 let mut cursor = node.walk();
1828 for child in node.named_children(&mut cursor) {
1829 if matches!(
1830 child.kind(),
1831 "block" | "statement_block" | "class_body" | "declaration_list"
1832 ) && let Some(docstring) = first_block_string_literal(child, content)
1833 {
1834 return Some(docstring);
1835 }
1836 }
1837 None
1838}
1839
1840fn first_block_string_literal(block: Node<'_>, content: &str) -> Option<String> {
1842 let mut cursor = block.walk();
1843 let first = block.named_children(&mut cursor).next()?;
1844 if first.kind() == "expression_statement" {
1845 let mut nested_cursor = first.walk();
1846 if let Some(string_node) = first
1847 .named_children(&mut nested_cursor)
1848 .find(|child| child.kind().contains("string"))
1849 {
1850 return clean_string_literal_doc(&node_text(string_node, content)?);
1851 }
1852 }
1853 if first.kind().contains("string") {
1854 return clean_string_literal_doc(&node_text(first, content)?);
1855 }
1856 None
1857}
1858
1859fn clean_string_literal_doc(value: &str) -> Option<String> {
1861 let trimmed = value.trim();
1862 let unquoted = trimmed
1863 .strip_prefix("\"\"\"")
1864 .and_then(|text| text.strip_suffix("\"\"\""))
1865 .or_else(|| {
1866 trimmed
1867 .strip_prefix("'''")
1868 .and_then(|text| text.strip_suffix("'''"))
1869 })
1870 .or_else(|| {
1871 trimmed
1872 .strip_prefix('"')
1873 .and_then(|text| text.strip_suffix('"'))
1874 })
1875 .or_else(|| {
1876 trimmed
1877 .strip_prefix('\'')
1878 .and_then(|text| text.strip_suffix('\''))
1879 })
1880 .unwrap_or(trimmed);
1881 compact_documentation(unquoted)
1882}
1883
1884fn compact_documentation(value: &str) -> Option<String> {
1886 let compact = value.split_whitespace().collect::<Vec<_>>().join(" ");
1887 if compact.is_empty() {
1888 None
1889 } else {
1890 Some(truncate_chars(&compact, MAX_DOC_CHARS))
1891 }
1892}
1893
1894fn enclosing_symbol_name(mut node: Option<Node<'_>>, content: &str) -> Option<String> {
1896 while let Some(current) = node {
1897 if declaration_kind(current.kind()).is_some()
1898 && let Some(name) = node_name(current, content)
1899 {
1900 return Some(name);
1901 }
1902 node = current.parent();
1903 }
1904 None
1905}
1906
1907fn node_text(node: Node<'_>, content: &str) -> Option<String> {
1909 node.utf8_text(content.as_bytes())
1910 .ok()
1911 .map(ToString::to_string)
1912}
1913
1914#[cfg(test)]
1916fn extract_fallback_graph(path: &str, language: Option<&str>, content: &str) -> SymbolGraph {
1917 match extract_fallback_graph_checked(path, language, content, &mut || Ok::<(), Infallible>(()))
1918 {
1919 Ok(graph) => graph,
1920 Err(unreachable) => match unreachable {},
1921 }
1922}
1923
1924fn extract_fallback_graph_checked<E>(
1926 path: &str,
1927 language: Option<&str>,
1928 content: &str,
1929 check: &mut impl FnMut() -> Result<(), E>,
1930) -> Result<SymbolGraph, E> {
1931 check()?;
1932 let mut graph = empty_graph(path, language, ParserKind::Fallback);
1933 let patterns = fallback_patterns();
1934 check()?;
1935 for (line_index, line) in content.lines().enumerate() {
1936 check_parser_iteration(line_index, check)?;
1937 let trimmed = line.trim();
1938 for pattern in &patterns {
1939 if let Some(capture) = pattern.regex.captures(trimmed)
1940 && let Some(name) = capture.get(1)
1941 {
1942 push_symbol(
1943 &mut graph,
1944 name.as_str(),
1945 pattern.kind,
1946 line_index + 1,
1947 line_index + 1,
1948 None,
1949 Some(pattern.detail),
1950 trimmed,
1951 );
1952 break;
1953 }
1954 }
1955 if is_fallback_import(trimmed) {
1956 push_relation(
1957 &mut graph,
1958 "<module>",
1959 trimmed,
1960 RelationKind::Imports,
1961 line_index + 1,
1962 trimmed,
1963 );
1964 }
1965 }
1966 check()?;
1967 languages::augment_fallback_language_graph(&mut graph, content, check)?;
1968 check()?;
1969 Ok(graph)
1970}
1971
1972struct FallbackPattern {
1974 regex: Regex,
1976 kind: SymbolKind,
1978 detail: &'static str,
1980}
1981
1982fn fallback_patterns() -> Vec<FallbackPattern> {
1984 let specs = [
1985 (
1986 r"^(?:async\s+)?def\s+([A-Za-z_][A-Za-z0-9_]*)",
1987 SymbolKind::Function,
1988 "fallback-python-function",
1989 ),
1990 (
1991 r"^class\s+([A-Za-z_][A-Za-z0-9_]*)",
1992 SymbolKind::Class,
1993 "fallback-class",
1994 ),
1995 (
1996 r"^function\s+([A-Za-z_][A-Za-z0-9_]*(?:-[A-Za-z_][A-Za-z0-9_]*)+)\b",
1997 SymbolKind::Function,
1998 "fallback-powershell-function",
1999 ),
2000 (
2001 r"^(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][A-Za-z0-9_$]*)",
2002 SymbolKind::Function,
2003 "fallback-js-function",
2004 ),
2005 (
2006 r"^(?:const|let|var)\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:withDefaults\s*\(\s*)?(?:defineProps|defineEmits|defineModel|defineSlots|computed|ref|shallowRef|reactive|toRef|toRefs|watch)\b",
2007 SymbolKind::Value,
2008 "fallback-composition-binding",
2009 ),
2010 (
2011 r"^(?:pub\s+)?fn\s+([A-Za-z_][A-Za-z0-9_]*)",
2012 SymbolKind::Function,
2013 "fallback-rust-function",
2014 ),
2015 (
2016 r"^(?:pub\s+)?(?:struct|enum|trait)\s+([A-Za-z_][A-Za-z0-9_]*)",
2017 SymbolKind::Type,
2018 "fallback-rust-type",
2019 ),
2020 (
2021 r"^(?:func|fun)\s+([A-Za-z_][A-Za-z0-9_]*)",
2022 SymbolKind::Function,
2023 "fallback-function",
2024 ),
2025 (
2026 r"^(?:public|private|protected|internal|static|\s)+\s*[A-Za-z0-9_<>,\[\]?]+\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(",
2027 SymbolKind::Method,
2028 "fallback-c-family-method",
2029 ),
2030 ];
2031 let mut patterns = Vec::new();
2032 for (source, kind, detail) in specs {
2033 if let Ok(regex) = Regex::new(source) {
2034 patterns.push(FallbackPattern {
2035 regex,
2036 kind,
2037 detail,
2038 });
2039 }
2040 }
2041 patterns
2042}
2043
2044fn is_fallback_import(line: &str) -> bool {
2046 matches!(
2047 line.split_whitespace().next(),
2048 Some("import" | "from" | "use" | "using" | "include" | "require")
2049 ) || line.starts_with("#include")
2050}
2051
2052fn empty_graph(path: &str, language: Option<&str>, parser: ParserKind) -> SymbolGraph {
2054 SymbolGraph {
2055 path: path.to_string(),
2056 language: language.map(ToString::to_string),
2057 parser,
2058 symbols: Vec::new(),
2059 relations: Vec::new(),
2060 }
2061}
2062
2063fn push_symbol(
2065 graph: &mut SymbolGraph,
2066 name: &str,
2067 kind: SymbolKind,
2068 line_start: usize,
2069 line_end: usize,
2070 parent: Option<String>,
2071 detail: Option<&str>,
2072 signature: &str,
2073) {
2074 push_symbol_with_metadata(
2075 graph, name, kind, line_start, line_end, parent, detail, signature, false, None,
2076 );
2077}
2078
2079fn push_symbol_with_metadata(
2081 graph: &mut SymbolGraph,
2082 name: &str,
2083 kind: SymbolKind,
2084 line_start: usize,
2085 line_end: usize,
2086 parent: Option<String>,
2087 detail: Option<&str>,
2088 signature: &str,
2089 exported: bool,
2090 documentation: Option<&str>,
2091) {
2092 if graph.symbols.len() >= MAX_SYMBOLS_PER_FILE {
2093 return;
2094 }
2095 let cleaned_name = compact_text(name);
2096 if cleaned_name.is_empty() {
2097 return;
2098 }
2099 graph.symbols.push(CodeSymbol {
2100 path: graph.path.clone(),
2101 language: graph.language.clone(),
2102 name: cleaned_name,
2103 kind,
2104 signature: truncate_chars_at_boundary(&compact_text(signature), MAX_SNIPPET_CHARS),
2105 exported,
2106 documentation: documentation.map(ToString::to_string),
2107 line_start,
2108 line_end: line_end.max(line_start),
2109 parent,
2110 parser: graph.parser,
2111 detail: detail.map(ToString::to_string),
2112 });
2113}
2114
2115fn push_relation(
2117 graph: &mut SymbolGraph,
2118 source_name: &str,
2119 target_name: &str,
2120 kind: RelationKind,
2121 line: usize,
2122 context: &str,
2123) {
2124 if graph.relations.len() >= MAX_RELATIONS_PER_FILE {
2125 return;
2126 }
2127 let target = compact_text(target_name);
2128 if target.is_empty() {
2129 return;
2130 }
2131 graph.relations.push(SymbolRelation {
2132 path: graph.path.clone(),
2133 source_name: truncate_chars_at_boundary(&compact_text(source_name), MAX_SNIPPET_CHARS),
2134 target_name: truncate_chars_at_boundary(&target, MAX_SNIPPET_CHARS),
2135 kind,
2136 line,
2137 context: truncate_chars_at_boundary(&compact_text(context), MAX_SNIPPET_CHARS),
2138 parser: graph.parser,
2139 });
2140}
2141
2142fn compact_text(value: &str) -> String {
2144 value.split_whitespace().collect::<Vec<_>>().join(" ")
2145}
2146
2147fn truncate_chars(value: &str, max_chars: usize) -> String {
2149 if value.chars().count() <= max_chars {
2150 return value.to_string();
2151 }
2152 value.chars().take(max_chars).collect()
2153}
2154
2155fn truncate_chars_at_boundary(value: &str, max_chars: usize) -> String {
2157 let value_chars = value.chars().count();
2158 if value_chars <= max_chars {
2159 return value.to_string();
2160 }
2161 let marker = "...";
2162 let marker_chars = marker.chars().count();
2163 if max_chars <= marker_chars {
2164 return value.chars().take(max_chars).collect();
2165 }
2166 let target_chars = max_chars - marker_chars;
2167 let mut fallback_end = 0_usize;
2168 let mut boundary_end = None;
2169 for (char_index, (index, character)) in value.char_indices().enumerate() {
2170 if char_index >= target_chars {
2171 break;
2172 }
2173 fallback_end = index + character.len_utf8();
2174 if is_snippet_boundary(character) {
2175 boundary_end = Some(fallback_end);
2176 }
2177 }
2178 let end = boundary_end.unwrap_or(fallback_end);
2179 let prefix = value[..end]
2180 .trim_end_matches(|character: char| {
2181 character.is_whitespace() || matches!(character, ',' | ';' | ':' | '{')
2182 })
2183 .to_string();
2184 if prefix.is_empty() {
2185 format!(
2186 "{}{marker}",
2187 value.chars().take(target_chars).collect::<String>()
2188 )
2189 } else {
2190 format!("{prefix}{marker}")
2191 }
2192}
2193
2194fn is_snippet_boundary(character: char) -> bool {
2196 character.is_whitespace()
2197 || matches!(
2198 character,
2199 ',' | ';' | ':' | '{' | '}' | '(' | ')' | '[' | ']' | '/' | '\\' | '.'
2200 )
2201}
2202
2203#[cfg(test)]
2204mod tests {
2205 use super::{
2206 MAX_SYMBOLS_PER_FILE, content_without_leading_purpose_header, empty_graph,
2207 extract_cargo_manifest_graph_checked, extract_fallback_graph,
2208 extract_fallback_graph_checked, extract_powershell_graph_checked, extract_symbol_graph,
2209 extract_symbol_graph_checked, extract_symbol_graph_controlled,
2210 extract_vue_sfc_graph_checked, languages, specialized_languages,
2211 };
2212 use projectatlas_core::symbols::{
2213 CodeSymbol, ParserKind, RelationKind, SymbolGraph, SymbolKind,
2214 };
2215 use projectatlas_core::{
2216 IndexCancellation, IndexWorkControl, IndexWorkFailure, IndexWorkStage,
2217 };
2218
2219 fn tree_symbol<'a>(
2220 graph: &'a SymbolGraph,
2221 kind: SymbolKind,
2222 name: &str,
2223 parent: Option<&str>,
2224 signature_fragment: &str,
2225 ) -> Option<&'a CodeSymbol> {
2226 graph.symbols.iter().find(|symbol| {
2227 symbol.parser == ParserKind::TreeSitter
2228 && symbol.kind == kind
2229 && symbol.name == name
2230 && symbol.parent.as_deref() == parent
2231 && symbol.signature.contains(signature_fragment)
2232 })
2233 }
2234
2235 #[test]
2236 fn controlled_extraction_consumes_cancellation_and_preserves_compatibility() {
2237 let fixtures = [
2238 (
2239 "src/lib.rs",
2240 Some("rust"),
2241 "pub struct Atlas;\nimpl Atlas { pub fn scan(&self) {} }\n",
2242 ),
2243 (
2244 "Cargo.toml",
2245 Some("cargo-manifest"),
2246 "[package]\nname = \"atlas\"\n[dependencies]\nserde = \"1\"\n",
2247 ),
2248 (
2249 "Cargo.lock",
2250 Some("cargo-lock"),
2251 "[[package]]\nname = \"atlas\"\nversion = \"0.1.0\"\n",
2252 ),
2253 (
2254 "src/App.vue",
2255 Some("vue"),
2256 "const props = defineProps<{ id: string }>()\n",
2257 ),
2258 (
2259 "scripts/Invoke-Atlas.ps1",
2260 Some("powershell"),
2261 "function Invoke-Atlas { return 1 }\n",
2262 ),
2263 (
2264 "config/atlas.txt",
2265 Some("text"),
2266 "function fallbackOnly() {}\n",
2267 ),
2268 (
2269 "src/Atlas.kt",
2270 Some("kotlin"),
2271 "package atlas\nclass Atlas {\nfun scan() {}\n}\n",
2272 ),
2273 ("build.gradle", Some("groovy"), "tasks.register('atlas')\n"),
2274 ];
2275 for &(path, language, source) in &fixtures {
2276 let expected = extract_symbol_graph(path, language, source);
2277 let active = IndexWorkControl::new(IndexCancellation::new(), None);
2278 assert_eq!(
2279 extract_symbol_graph_controlled(path, language, source, &active),
2280 Ok(expected),
2281 "controlled extraction changed compatibility output for {path}"
2282 );
2283 }
2284
2285 let cancellation = IndexCancellation::new();
2286 let control = IndexWorkControl::new(cancellation.clone(), None);
2287 let mut checkpoints = 0;
2288 let cancelled =
2289 extract_symbol_graph_checked("src/lib.rs", Some("rust"), fixtures[0].2, &mut || {
2290 checkpoints += 1;
2291 if checkpoints == 3 {
2292 cancellation.cancel();
2293 }
2294 control.check(IndexWorkStage::SymbolParsing)
2295 });
2296 assert_eq!(
2297 cancelled,
2298 Err(IndexWorkFailure::Cancelled {
2299 stage: IndexWorkStage::SymbolParsing,
2300 })
2301 );
2302 assert_eq!(checkpoints, 3);
2303
2304 macro_rules! assert_parser_cancels_at {
2305 ($label:expr, $checkpoint:expr, $run:expr) => {{
2306 let cancellation = IndexCancellation::new();
2307 let control = IndexWorkControl::new(cancellation.clone(), None);
2308 let mut checkpoints = 0;
2309 let mut check = || {
2310 checkpoints += 1;
2311 if checkpoints == $checkpoint {
2312 cancellation.cancel();
2313 }
2314 control.check(IndexWorkStage::SymbolParsing)
2315 };
2316 let cancelled = ($run)(&mut check);
2317 assert_eq!(
2318 cancelled,
2319 Err(IndexWorkFailure::Cancelled {
2320 stage: IndexWorkStage::SymbolParsing,
2321 }),
2322 "{} did not observe cancellation inside its owned parser loop",
2323 $label
2324 );
2325 assert_eq!(
2326 checkpoints, $checkpoint,
2327 "unexpected parser checkpoint path for {}",
2328 $label
2329 );
2330 }};
2331 }
2332
2333 assert_parser_cancels_at!("Cargo manifest", 3, |check| {
2334 extract_cargo_manifest_graph_checked(fixtures[1].0, fixtures[1].1, fixtures[1].2, check)
2335 });
2336 assert_parser_cancels_at!("fallback", 3, |check| {
2337 extract_fallback_graph_checked(fixtures[5].0, fixtures[5].1, fixtures[5].2, check)
2338 });
2339 assert_parser_cancels_at!("Vue structural adapter", 8, |check| {
2340 extract_vue_sfc_graph_checked(fixtures[3].0, fixtures[3].1, fixtures[3].2, check)
2341 });
2342 assert_parser_cancels_at!("PowerShell structural adapter", 8, |check| {
2343 extract_powershell_graph_checked(fixtures[4].0, fixtures[4].1, fixtures[4].2, check)
2344 });
2345
2346 let mut native_augmentation =
2347 empty_graph(fixtures[6].0, fixtures[6].1, ParserKind::TreeSitter);
2348 assert_parser_cancels_at!("native language augmentation", 3, |check| {
2349 languages::augment_language_graph(&mut native_augmentation, fixtures[6].2, check)
2350 .map(|()| native_augmentation.clone())
2351 });
2352
2353 let mut fallback_augmentation =
2354 empty_graph(fixtures[7].0, fixtures[7].1, ParserKind::Fallback);
2355 assert_parser_cancels_at!("fallback language augmentation", 4, |check| {
2356 languages::augment_fallback_language_graph(
2357 &mut fallback_augmentation,
2358 fixtures[7].2,
2359 check,
2360 )
2361 .map(|()| fallback_augmentation.clone())
2362 });
2363 }
2364
2365 #[test]
2366 fn supplied_language_selects_specialized_symbol_owner() {
2367 let cargo = extract_symbol_graph(
2368 "config/manifest.txt",
2369 Some("cargo-manifest"),
2370 "[package]\nname = \"atlas\"\n",
2371 );
2372 assert_eq!(cargo.parser, ParserKind::Manifest);
2373 assert!(
2374 cargo
2375 .symbols
2376 .iter()
2377 .any(|symbol| { symbol.kind == SymbolKind::Package && symbol.name == "atlas" })
2378 );
2379
2380 let vue = extract_symbol_graph(
2381 "config/component.txt",
2382 Some("vue"),
2383 "const count = ref(0);\n",
2384 );
2385 assert_eq!(vue.parser, ParserKind::Structural);
2386 assert!(vue.symbols.iter().any(|symbol| {
2387 symbol.name == "count" && symbol.detail.as_deref() == Some("vue-composition-binding")
2388 }));
2389
2390 let powershell = extract_symbol_graph(
2391 "config/script.txt",
2392 Some("powershell"),
2393 "function Get-Atlas { return 1 }\n",
2394 );
2395 assert_eq!(powershell.parser, ParserKind::Structural);
2396 assert!(powershell.symbols.iter().any(|symbol| {
2397 symbol.name == "Get-Atlas" && symbol.detail.as_deref() == Some("powershell-function")
2398 }));
2399
2400 for (path, source, forbidden_detail) in [
2401 (
2402 "Cargo.toml",
2403 "[package]\nname = \"atlas\"\n",
2404 "cargo-package",
2405 ),
2406 (
2407 "src/App.vue",
2408 "const count = ref(0);\n",
2409 "vue-composition-binding",
2410 ),
2411 (
2412 "scripts/Get-Atlas.ps1",
2413 "function Get-Atlas { return 1 }\n",
2414 "powershell-function",
2415 ),
2416 ] {
2417 let overridden = extract_symbol_graph(path, Some("text"), source);
2418 assert_eq!(overridden.parser, ParserKind::Structural, "{path}");
2419 assert!(overridden.symbols.is_empty(), "{path}");
2420 assert!(
2421 overridden
2422 .symbols
2423 .iter()
2424 .all(|symbol| symbol.detail.as_deref() != Some(forbidden_detail)),
2425 "{path} ignored its supplied language: {:?}",
2426 overridden.symbols
2427 );
2428 }
2429
2430 let cargo_lock_override = extract_symbol_graph(
2431 "Cargo.toml",
2432 Some("cargo-lock"),
2433 "[[package]]\nname = \"atlas-lock\"\nversion = \"1.0.0\"\n",
2434 );
2435 assert!(cargo_lock_override.symbols.iter().any(|symbol| {
2436 symbol.kind == SymbolKind::Dependency && symbol.name == "atlas-lock"
2437 }));
2438 }
2439
2440 #[test]
2441 fn unavailable_symbol_owner_does_not_run_fallback_extraction() {
2442 let graph = extract_symbol_graph(
2443 "README.md",
2444 Some("markdown"),
2445 "pub fn forged_symbol() {}\nclass ForgedType {}\n",
2446 );
2447
2448 assert_eq!(graph.parser, ParserKind::Structural);
2449 assert!(graph.symbols.is_empty());
2450 assert!(graph.relations.is_empty());
2451 }
2452
2453 #[test]
2454 fn missing_language_preserves_specialized_symbol_path_inference() {
2455 let cargo = extract_symbol_graph("Cargo.toml", None, "[package]\nname = \"atlas\"\n");
2456 assert_eq!(cargo.parser, ParserKind::Manifest);
2457 assert!(
2458 cargo
2459 .symbols
2460 .iter()
2461 .any(|symbol| symbol.kind == SymbolKind::Package)
2462 );
2463
2464 let vue = extract_symbol_graph("src/App.vue", None, "const count = ref(0);\n");
2465 assert_eq!(vue.parser, ParserKind::Structural);
2466 assert!(
2467 vue.symbols
2468 .iter()
2469 .any(|symbol| { symbol.detail.as_deref() == Some("vue-composition-binding") })
2470 );
2471
2472 let powershell = extract_symbol_graph(
2473 "scripts/Get-Atlas.ps1",
2474 None,
2475 "function Get-Atlas { return 1 }\n",
2476 );
2477 assert_eq!(powershell.parser, ParserKind::Structural);
2478 assert!(
2479 powershell
2480 .symbols
2481 .iter()
2482 .any(|symbol| { symbol.detail.as_deref() == Some("powershell-function") })
2483 );
2484 }
2485
2486 #[test]
2487 fn extracts_rust_symbols_and_calls() {
2488 let source = r"
2489use std::fs;
2490
2491pub struct Atlas;
2492
2493impl Atlas {
2494 /// Run the atlas scan.
2495 pub fn scan(&self) {
2496 helper();
2497 }
2498}
2499
2500fn helper() {}
2501";
2502 let graph = extract_symbol_graph("src/lib.rs", Some("rust"), source);
2503 assert!(
2504 graph.symbols.iter().any(|symbol| {
2505 symbol.kind == SymbolKind::Struct && symbol.name.contains("Atlas")
2506 })
2507 );
2508 assert!(graph.symbols.iter().any(|symbol| {
2509 symbol.kind == SymbolKind::Function && symbol.name.contains("helper")
2510 }));
2511 assert!(graph.symbols.iter().any(|symbol| {
2512 symbol.kind == SymbolKind::Method
2513 && symbol.name.contains("scan")
2514 && symbol.parent.as_deref() == Some("Atlas")
2515 && symbol.exported
2516 && symbol.documentation.as_deref() == Some("Run the atlas scan.")
2517 }));
2518 assert!(graph.relations.iter().any(|relation| {
2519 relation.kind == RelationKind::Calls && relation.target_name.contains("helper")
2520 }));
2521 }
2522
2523 #[test]
2524 fn declaration_signatures_ignore_location_formatting_and_body_edits() {
2525 let before = extract_symbol_graph(
2526 "src/lib.rs",
2527 Some("rust"),
2528 "struct Atlas;\nimpl Atlas { pub fn run(&self, value: i32) -> i32 { value + 1 } }\n",
2529 );
2530 let after = extract_symbol_graph(
2531 "src/lib.rs",
2532 Some("rust"),
2533 "\n// moved\nstruct Atlas;\nimpl Atlas {\n pub fn run(\n &self,\n value: i32\n ) -> i32 {\n value.saturating_mul(20)\n }\n}\n",
2534 );
2535 let before = tree_symbol(&before, SymbolKind::Method, "run", Some("Atlas"), "i32");
2536 let after = tree_symbol(&after, SymbolKind::Method, "run", Some("Atlas"), "i32");
2537 assert!(before.is_some() && after.is_some());
2538 let (Some(before), Some(after)) = (before, after) else {
2539 return;
2540 };
2541 assert_ne!(before.line_start, after.line_start);
2542 assert_eq!(before.signature, after.signature);
2543 assert!(!after.signature.contains("saturating_mul"));
2544
2545 let first = extract_symbol_graph(
2546 "src/run.ts",
2547 Some("typescript"),
2548 "export const run = (value: number): number => { return value + 1; };",
2549 );
2550 let changed = extract_symbol_graph(
2551 "src/run.ts",
2552 Some("typescript"),
2553 "export const run = (value: number): number => { return value * 20; };",
2554 );
2555 let first = tree_symbol(&first, SymbolKind::Function, "run", None, "number");
2556 let changed = tree_symbol(&changed, SymbolKind::Function, "run", None, "number");
2557 assert!(first.is_some() && changed.is_some());
2558 let (Some(first), Some(changed)) = (first, changed) else {
2559 return;
2560 };
2561 assert_eq!(first.signature, changed.signature);
2562 assert!(!first.signature.contains("return"));
2563 }
2564
2565 #[test]
2566 fn declaration_value_signatures_ignore_initializers_but_retain_declared_types() {
2567 for (path, language, before, initializer_changed, type_changed, name, declared_type) in [
2568 (
2569 "src/lib.rs",
2570 "rust",
2571 "pub const LIMIT: usize = 10;",
2572 "pub const LIMIT: usize = calculate_limit();",
2573 "pub const LIMIT: u64 = 10;",
2574 "LIMIT",
2575 "usize",
2576 ),
2577 (
2578 "src/config.ts",
2579 "typescript",
2580 "export const retryCount: number = 3;",
2581 "export const retryCount: number = calculateRetries();",
2582 "export const retryCount: string = '3';",
2583 "retryCount",
2584 "number",
2585 ),
2586 ] {
2587 let before = extract_symbol_graph(path, Some(language), before);
2588 let initializer_changed =
2589 extract_symbol_graph(path, Some(language), initializer_changed);
2590 let type_changed = extract_symbol_graph(path, Some(language), type_changed);
2591 let before = tree_symbol(&before, SymbolKind::Value, name, None, declared_type);
2592 let initializer_changed = tree_symbol(
2593 &initializer_changed,
2594 SymbolKind::Value,
2595 name,
2596 None,
2597 declared_type,
2598 );
2599 let type_changed = tree_symbol(&type_changed, SymbolKind::Value, name, None, "");
2600 assert!(before.is_some() && initializer_changed.is_some() && type_changed.is_some());
2601 let (Some(before), Some(initializer_changed), Some(type_changed)) =
2602 (before, initializer_changed, type_changed)
2603 else {
2604 return;
2605 };
2606 assert_eq!(before.signature, initializer_changed.signature);
2607 assert_ne!(before.signature, type_changed.signature);
2608 assert!(!initializer_changed.signature.contains("calculate"));
2609 }
2610 }
2611
2612 #[test]
2613 fn declaration_identity_material_disambiguates_overloads_and_parents() {
2614 let rust = extract_symbol_graph(
2615 "src/lib.rs",
2616 Some("rust"),
2617 "struct Left; impl Left { fn run(&self, value: i32) {} }\nstruct Right; impl Right { fn run(&self, value: i32) {} }\n",
2618 );
2619 let left = tree_symbol(&rust, SymbolKind::Method, "run", Some("Left"), "i32");
2620 let right = tree_symbol(&rust, SymbolKind::Method, "run", Some("Right"), "i32");
2621 assert!(left.is_some() && right.is_some());
2622 let (Some(left), Some(right)) = (left, right) else {
2623 return;
2624 };
2625 assert_eq!(left.signature, right.signature);
2626 assert_ne!(left.parent, right.parent);
2627
2628 let java = extract_symbol_graph(
2629 "src/Runner.java",
2630 Some("java"),
2631 "class Runner { int run(\n int value\n) { return value; } String run(\n String value\n) { return value; } }",
2632 );
2633 let numeric = tree_symbol(
2634 &java,
2635 SymbolKind::Method,
2636 "run",
2637 Some("Runner"),
2638 "int value",
2639 );
2640 let textual = tree_symbol(
2641 &java,
2642 SymbolKind::Method,
2643 "run",
2644 Some("Runner"),
2645 "String value",
2646 );
2647 assert!(numeric.is_some() && textual.is_some());
2648 let (Some(numeric), Some(textual)) = (numeric, textual) else {
2649 return;
2650 };
2651 assert_ne!(numeric.signature, textual.signature);
2652 assert_eq!(numeric.parent, textual.parent);
2653 }
2654
2655 #[test]
2656 fn native_parser_graph_survives_when_empty() {
2657 let graph = extract_symbol_graph("src/empty.rs", Some("rust"), "// comment only\n");
2658 assert_eq!(graph.parser, ParserKind::TreeSitter);
2659 assert!(graph.symbols.is_empty());
2660 assert!(graph.relations.is_empty());
2661 }
2662
2663 #[test]
2664 fn native_parser_ignores_fallback_patterns_inside_comments() {
2665 let graph = extract_symbol_graph(
2666 "src/commented.rs",
2667 Some("rust"),
2668 "/*\ndef leaked():\n pass\nfunction leaked() {}\nimport x\n*/\n",
2669 );
2670 assert_eq!(graph.parser, ParserKind::TreeSitter);
2671 assert!(graph.symbols.is_empty());
2672 assert!(graph.relations.is_empty());
2673
2674 let graph = extract_symbol_graph(
2675 "src/commented.ts",
2676 Some("typescript"),
2677 "/*\ndef leaked():\n pass\nfunction leaked() {}\nimport x\n*/\n",
2678 );
2679 assert_eq!(graph.parser, ParserKind::TreeSitter);
2680 assert!(graph.symbols.is_empty());
2681 assert!(graph.relations.is_empty());
2682 }
2683
2684 #[test]
2685 fn native_empty_graph_keeps_fallback_rescue() {
2686 let graph = extract_symbol_graph(
2687 "src/misdetected.rs",
2688 Some("rust"),
2689 "def rescued():\n return 1\n",
2690 );
2691 assert_eq!(graph.parser, ParserKind::Fallback);
2692 assert!(graph.symbols.iter().any(|symbol| {
2693 symbol.name == "rescued"
2694 && symbol.kind == SymbolKind::Function
2695 && symbol.detail.as_deref() == Some("fallback-python-function")
2696 }));
2697 }
2698
2699 #[test]
2700 fn fallback_preserves_full_powershell_function_names() {
2701 let graph = extract_symbol_graph(
2702 "scripts/install-runtime.ps1",
2703 Some("powershell"),
2704 "class RuntimeConfig {\n}\nfunction Resolve-DefaultProjectRoot {\n}\nfunction Get-ReleaseRuntimeInstallPath {\n}\nfunction Install-ReleaseBinary {\n}\n",
2705 );
2706 assert_eq!(graph.parser, ParserKind::Structural);
2707 assert!(
2708 graph.symbols.iter().any(|symbol| {
2709 symbol.kind == SymbolKind::Class
2710 && symbol.name == "RuntimeConfig"
2711 && symbol.detail.as_deref() == Some("powershell-class")
2712 }),
2713 "missing PowerShell class symbol: {:?}",
2714 graph.symbols
2715 );
2716
2717 for name in [
2718 "Resolve-DefaultProjectRoot",
2719 "Get-ReleaseRuntimeInstallPath",
2720 "Install-ReleaseBinary",
2721 ] {
2722 assert!(
2723 graph.symbols.iter().any(|symbol| {
2724 symbol.kind == SymbolKind::Function
2725 && symbol.name == name
2726 && symbol.detail.as_deref() == Some("powershell-function")
2727 }),
2728 "missing full PowerShell function name {name}: {:?}",
2729 graph.symbols
2730 );
2731 }
2732 assert!(
2733 !graph
2734 .symbols
2735 .iter()
2736 .any(|symbol| symbol.name == "Resolve" || symbol.name == "Install"),
2737 "PowerShell function names must not be truncated to verbs: {:?}",
2738 graph.symbols
2739 );
2740 }
2741
2742 #[test]
2743 fn extracts_typescript_symbols() {
2744 let source = r#"
2745import { readFile } from "fs";
2746export interface Reader { read(): string }
2747export class AtlasReader {
2748 read() { return readFile; }
2749}
2750export function createReader() { return new AtlasReader(); }
2751export const createWriter = () => createReader();
2752"#;
2753 let graph = extract_symbol_graph("src/index.ts", Some("typescript"), source);
2754 assert!(graph.symbols.iter().any(|symbol| {
2755 symbol.kind == SymbolKind::Interface
2756 && symbol.name.contains("Reader")
2757 && symbol.exported
2758 }));
2759 assert!(graph.symbols.iter().any(|symbol| {
2760 symbol.kind == SymbolKind::Class
2761 && symbol.name.contains("AtlasReader")
2762 && symbol.exported
2763 }));
2764 assert!(graph.symbols.iter().any(|symbol| {
2765 symbol.kind == SymbolKind::Function
2766 && symbol.name.contains("createReader")
2767 && symbol.exported
2768 }));
2769 assert!(graph.symbols.iter().any(|symbol| {
2770 symbol.kind == SymbolKind::Function && symbol.name == "createWriter" && symbol.exported
2771 }));
2772 assert!(graph.symbols.iter().any(|symbol| {
2773 symbol.kind == SymbolKind::Method
2774 && symbol.name == "read"
2775 && symbol.parent.as_deref() == Some("AtlasReader")
2776 }));
2777 assert!(graph.relations.iter().any(|relation| {
2778 relation.kind == RelationKind::Imports && relation.target_name.contains("readFile")
2779 }));
2780 }
2781
2782 #[test]
2783 fn typescript_nested_locals_do_not_inherit_exported_parent() {
2784 let source = r#"
2785export function useAtlas() {
2786 type LocalMode = "fast" | "safe";
2787 const localCache = new Map<string, string>();
2788 const computeLocal = () => localCache.size;
2789 return computeLocal();
2790}
2791"#;
2792 let graph = extract_symbol_graph("src/use-atlas.ts", Some("typescript"), source);
2793 assert!(graph.symbols.iter().any(|symbol| {
2794 symbol.kind == SymbolKind::Function && symbol.name == "useAtlas" && symbol.exported
2795 }));
2796 assert!(graph.symbols.iter().any(|symbol| {
2797 symbol.name == "LocalMode"
2798 && symbol.parent.as_deref() == Some("useAtlas")
2799 && !symbol.exported
2800 }));
2801 for nested_value in ["localCache", "computeLocal"] {
2802 assert!(
2803 graph.symbols.iter().any(|symbol| {
2804 symbol.name == nested_value
2805 && symbol.kind == SymbolKind::Value
2806 && symbol.parent.as_deref() == Some("useAtlas")
2807 && !symbol.exported
2808 }),
2809 "nested value {nested_value} should remain indexed with parent and no export"
2810 );
2811 }
2812 }
2813
2814 #[test]
2815 fn javascript_summary_symbols_ignore_locals_and_iife_constants() {
2816 let source = r#"
2817import path from "node:path";
2818import { createHash } from "node:crypto";
2819
2820const DATA_DIRECTORY = path.resolve("app/public/data");
2821const OUTPUT_FILE = path.join(DATA_DIRECTORY, "datasets.manifest.json");
2822const CACHE_NAME = (() => `sw-${Date.now()}`)();
2823
2824function sha256(value) {
2825 return createHash("sha256").update(value).digest("hex");
2826}
2827
2828async function readDatasetEntry(filePath) {
2829 return sha256(filePath);
2830}
2831
2832async function main() {
2833 const datasetEntries = await Promise.all(["a"].map((file) => readDatasetEntry(file)));
2834 const versionSeed = datasetEntries.map((entry) => entry.id).join("\n");
2835 return versionSeed;
2836}
2837"#;
2838 let graph = extract_symbol_graph("scripts/generate.mjs", Some("javascript"), source);
2839 for name in ["sha256", "readDatasetEntry", "main"] {
2840 assert!(
2841 graph
2842 .symbols
2843 .iter()
2844 .any(|symbol| symbol.kind == SymbolKind::Function && symbol.name == name),
2845 "missing top-level function {name}"
2846 );
2847 }
2848 for name in ["DATA_DIRECTORY", "OUTPUT_FILE", "CACHE_NAME"] {
2849 assert!(
2850 graph
2851 .symbols
2852 .iter()
2853 .any(|symbol| symbol.kind == SymbolKind::Value && symbol.name == name),
2854 "missing top-level constant {name}"
2855 );
2856 assert!(
2857 !graph
2858 .symbols
2859 .iter()
2860 .any(|symbol| symbol.kind == SymbolKind::Function && symbol.name == name),
2861 "constant {name} must not be promoted to a function"
2862 );
2863 }
2864 for local in ["datasetEntries", "versionSeed"] {
2865 assert!(
2866 graph
2867 .symbols
2868 .iter()
2869 .any(|symbol| symbol.kind == SymbolKind::Value && symbol.name == local),
2870 "local binding {local} should remain indexed as a nested value"
2871 );
2872 assert!(
2873 !graph
2874 .symbols
2875 .iter()
2876 .any(|symbol| symbol.kind == SymbolKind::Function && symbol.name == local),
2877 "local binding {local} must not become a function"
2878 );
2879 }
2880 }
2881
2882 #[test]
2883 fn javascript_object_literal_methods_are_not_file_level_methods() {
2884 let source = r"
2885const stub = {
2886 addListener() {},
2887 removeListener() {},
2888 nested: {
2889 addEventListener() {},
2890 removeEventListener() {}
2891 }
2892};
2893
2894class Harness {
2895 run() {}
2896}
2897";
2898 let graph = extract_symbol_graph("tests/browser.spec.js", Some("javascript"), source);
2899 for object_method in [
2900 "addListener",
2901 "removeListener",
2902 "addEventListener",
2903 "removeEventListener",
2904 ] {
2905 assert!(
2906 !graph
2907 .symbols
2908 .iter()
2909 .any(|symbol| symbol.name == object_method),
2910 "object literal method {object_method} must not become a file-level method"
2911 );
2912 }
2913 assert!(
2914 graph
2915 .symbols
2916 .iter()
2917 .any(|symbol| { symbol.kind == SymbolKind::Class && symbol.name == "Harness" })
2918 );
2919 assert!(graph.symbols.iter().any(|symbol| {
2920 symbol.kind == SymbolKind::Method
2921 && symbol.name == "run"
2922 && symbol.parent.as_deref() == Some("Harness")
2923 }));
2924 }
2925
2926 #[test]
2927 fn javascript_exported_object_literal_methods_remain_indexed() {
2928 let source = r"
2929export const api = {
2930 list() {},
2931 nested: {
2932 refresh() {}
2933 }
2934};
2935
2936module.exports = {
2937 boot() {}
2938};
2939";
2940 let graph = extract_symbol_graph("src/api.js", Some("javascript"), source);
2941 assert!(graph.symbols.iter().any(|symbol| {
2942 symbol.kind == SymbolKind::Method
2943 && symbol.name == "list"
2944 && symbol.parent.as_deref() == Some("api")
2945 && symbol.exported
2946 }));
2947 assert!(graph.symbols.iter().any(|symbol| {
2948 symbol.kind == SymbolKind::Method
2949 && symbol.name == "refresh"
2950 && symbol.parent.as_deref() == Some("api.nested")
2951 && symbol.exported
2952 }));
2953 assert!(graph.symbols.iter().any(|symbol| {
2954 symbol.kind == SymbolKind::Method
2955 && symbol.name == "boot"
2956 && symbol.parent.as_deref() == Some("module.exports")
2957 && symbol.exported
2958 }));
2959 }
2960
2961 #[test]
2962 fn javascript_direct_callable_constants_remain_functions() {
2963 let source = r#"
2964export const createThing = () => ({ kind: "thing" });
2965const helper = function helperFactory() { return createThing(); };
2966"#;
2967 let graph = extract_symbol_graph("src/factory.js", Some("javascript"), source);
2968 for name in ["createThing", "helper"] {
2969 assert!(
2970 graph
2971 .symbols
2972 .iter()
2973 .any(|symbol| symbol.kind == SymbolKind::Function && symbol.name == name),
2974 "callable constant {name} should remain function-like"
2975 );
2976 }
2977 }
2978
2979 #[test]
2980 fn file_purpose_docblock_is_not_symbol_documentation() {
2981 let source = r#"/**
2982 * Purpose: Choose a fresher catalog start so repeated app opens avoid the same opening items.
2983 */
2984import type { CatalogItem } from "@/types/catalog";
2985export function applyLaunchFreshness() {}
2986"#;
2987 let graph = extract_symbol_graph("src/launch-freshness.ts", Some("typescript"), source);
2988 assert!(
2989 graph
2990 .symbols
2991 .iter()
2992 .any(|symbol| symbol.kind == SymbolKind::Import && symbol.documentation.is_none())
2993 );
2994 assert!(
2995 graph
2996 .symbols
2997 .iter()
2998 .any(|symbol| symbol.name == "applyLaunchFreshness"
2999 && symbol.documentation.is_none())
3000 );
3001 }
3002
3003 #[test]
3004 fn boundary_truncates_long_import_snippet() {
3005 let truncated = super::truncate_chars_at_boundary(
3006 "import type { DigestDraft, DeliveryChannel, CatalogDatasetItem } from \"@/catalog\";",
3007 56,
3008 );
3009
3010 assert_eq!(truncated, "import type { DigestDraft, DeliveryChannel...");
3011 }
3012
3013 #[test]
3014 fn import_specific_comment_remains_import_documentation() {
3015 let source = r#"/** Loads a required browser polyfill. */
3016import "./polyfill";
3017"#;
3018 let graph = extract_symbol_graph("src/polyfills.ts", Some("typescript"), source);
3019 assert!(
3020 graph.symbols.iter().any(|symbol| {
3021 symbol.kind == SymbolKind::Import
3022 && symbol.documentation.as_deref() == Some("Loads a required browser polyfill.")
3023 }),
3024 "import-specific documentation should remain attached to the import symbol"
3025 );
3026 }
3027
3028 #[test]
3029 fn extracts_vue_composition_bindings_from_script_setup() {
3030 let source = r#"
3031<template><article>{{ currentPriceLabel }}</article></template>
3032<script setup lang="ts">
3033import { computed, ref } from "vue";
3034
3035const props = withDefaults(defineProps<{
3036 title: string;
3037}>(), { title: "Product" });
3038const emit = defineEmits<{
3039 select: [id: string];
3040}>();
3041const productTitleId = computed(() => props.title.toLowerCase());
3042const currentPriceLabel = computed(() => `$${props.title}`);
3043const retryCount = ref(0);
3044</script>
3045"#;
3046 let graph = extract_symbol_graph("src/ProductPanel.vue", Some("vue"), source);
3047 for expected in [
3048 "props",
3049 "emit",
3050 "productTitleId",
3051 "currentPriceLabel",
3052 "retryCount",
3053 ] {
3054 assert!(
3055 graph.symbols.iter().any(|symbol| {
3056 symbol.kind == SymbolKind::Value
3057 && symbol.name == expected
3058 && symbol.detail.as_deref() == Some("vue-composition-binding")
3059 && symbol.parser == ParserKind::Structural
3060 }),
3061 "missing Vue Composition API binding {expected}"
3062 );
3063 }
3064 assert_eq!(graph.parser, ParserKind::Structural);
3065 assert!(graph.relations.iter().any(|relation| {
3066 relation.kind == RelationKind::Imports
3067 && relation.target_name.contains("computed")
3068 && relation.parser == ParserKind::Structural
3069 }));
3070 assert!(
3071 graph
3072 .symbols
3073 .iter()
3074 .all(|symbol| symbol.parser == ParserKind::Structural)
3075 );
3076 assert!(graph.relations.iter().any(|relation| {
3077 relation.kind == RelationKind::Calls
3078 && relation.target_name == "computed"
3079 && relation.parser == ParserKind::TreeSitter
3080 }));
3081 }
3082
3083 #[test]
3084 fn extracts_embedded_html_and_svelte_facts_at_host_lines() {
3085 let html = r#"<main>not source</main>
3086<script lang="ts">
3087export interface ProductRecord { id: string }
3088export function loadProduct() { return ProductRecord; }
3089</script>
3090"#;
3091 let graph = extract_symbol_graph("public/index.html", Some("html"), html);
3092 assert_eq!(graph.parser, ParserKind::Structural);
3093 assert!(graph.symbols.iter().any(|symbol| {
3094 symbol.name == "ProductRecord"
3095 && symbol.kind == SymbolKind::Interface
3096 && symbol.line_start == 3
3097 && symbol.parser == ParserKind::TreeSitter
3098 && symbol.language.as_deref() == Some("typescript")
3099 }));
3100 assert!(graph.symbols.iter().any(|symbol| {
3101 symbol.name == "loadProduct"
3102 && symbol.line_start == 4
3103 && symbol.parser == ParserKind::TreeSitter
3104 }));
3105
3106 let svelte = r#"<h1>{title}</h1>
3107<script lang="ts">
3108export interface PageData { title: string }
3109</script>
3110"#;
3111 let graph = extract_symbol_graph("src/Page.svelte", Some("svelte"), svelte);
3112 assert_eq!(graph.parser, ParserKind::Fallback);
3113 assert!(graph.symbols.iter().any(|symbol| {
3114 symbol.name == "PageData"
3115 && symbol.kind == SymbolKind::Interface
3116 && symbol.line_start == 3
3117 && symbol.parser == ParserKind::TreeSitter
3118 }));
3119 }
3120
3121 #[test]
3122 fn embedded_hosts_ignore_external_and_retain_only_safe_facts_on_incomplete_input() {
3123 for source in [
3124 "<script src=\"external.js\">export function forged() {}</script>",
3125 "<script lang=\"ts\">export function incomplete() {}",
3126 ] {
3127 let graph = extract_symbol_graph("public/index.html", Some("html"), source);
3128 assert!(
3129 graph.symbols.is_empty(),
3130 "unexpected symbols: {:?}",
3131 graph.symbols
3132 );
3133 assert!(
3134 graph.relations.is_empty(),
3135 "unexpected relations: {:?}",
3136 graph.relations
3137 );
3138 }
3139
3140 let source = "<script>export function admitted() {}</script>"
3141 .repeat(super::semantic::embedded_source::MAX_EMBEDDED_SCRIPT_REGIONS + 1);
3142 let graph = extract_symbol_graph("public/index.html", Some("html"), &source);
3143 assert!(graph.symbols.iter().any(|symbol| symbol.name == "admitted"));
3144 assert_eq!(graph.parser, ParserKind::Structural);
3145 }
3146
3147 #[test]
3148 fn purpose_header_mask_preserves_utf8_byte_offsets_for_embedded_hosts() {
3149 let source = concat!(
3150 "<!-- Purpose: Grüße and routing -->\n",
3151 "<script lang=\"ts\">export const admitted = 1;</script>\n"
3152 );
3153 let masked = content_without_leading_purpose_header(source);
3154 assert_eq!(masked.len(), source.len());
3155 assert_eq!(masked.find("<script"), source.find("<script"));
3156
3157 let graph = extract_symbol_graph("public/index.html", Some("html"), source);
3158 assert!(graph.symbols.iter().any(|symbol| {
3159 symbol.name == "admitted"
3160 && symbol.line_start == 2
3161 && symbol.parser == ParserKind::TreeSitter
3162 }));
3163 }
3164
3165 #[test]
3166 fn vue_sfc_preserves_fallback_declarations() {
3167 let source = r#"
3168<script lang="ts">
3169export function submitOrder() {
3170 return true;
3171}
3172
3173class Store {
3174}
3175</script>
3176<script setup lang="ts">
3177import { ref } from "vue";
3178const selected = ref(false);
3179</script>
3180"#;
3181 let graph = extract_symbol_graph("src/CheckoutPanel.vue", Some("vue"), source);
3182
3183 assert!(graph.symbols.iter().any(|symbol| {
3184 symbol.kind == SymbolKind::Value
3185 && symbol.name == "selected"
3186 && symbol.detail.as_deref() == Some("vue-composition-binding")
3187 && symbol.parser == ParserKind::Structural
3188 }));
3189 assert!(graph.symbols.iter().any(|symbol| {
3190 symbol.kind == SymbolKind::Function
3191 && symbol.name == "submitOrder"
3192 && symbol.detail.as_deref() == Some("fallback-js-function")
3193 && symbol.parser == ParserKind::Fallback
3194 }));
3195 assert!(graph.symbols.iter().any(|symbol| {
3196 symbol.kind == SymbolKind::Class
3197 && symbol.name == "Store"
3198 && symbol.detail.as_deref() == Some("fallback-class")
3199 && symbol.parser == ParserKind::Fallback
3200 }));
3201 }
3202
3203 #[test]
3204 fn vue_sfc_preserves_fallback_declarations_when_bindings_exceed_cap() {
3205 let mut source = String::from(
3206 r#"
3207<script setup lang="ts">
3208export function submitOrder() {
3209 return true;
3210}
3211
3212class Store {
3213}
3214"#,
3215 );
3216 for index in 0..(MAX_SYMBOLS_PER_FILE + 50) {
3217 source.push_str("const value");
3218 source.push_str(&index.to_string());
3219 source.push_str(" = ref(false);\n");
3220 }
3221 source.push_str("</script>\n");
3222
3223 let graph = extract_symbol_graph("src/LargePanel.vue", Some("vue"), &source);
3224
3225 assert!(graph.symbols.iter().any(|symbol| {
3226 symbol.kind == SymbolKind::Function
3227 && symbol.name == "submitOrder"
3228 && symbol.detail.as_deref() == Some("fallback-js-function")
3229 }));
3230 assert!(graph.symbols.iter().any(|symbol| {
3231 symbol.kind == SymbolKind::Class
3232 && symbol.name == "Store"
3233 && symbol.detail.as_deref() == Some("fallback-class")
3234 }));
3235 assert!(graph.symbols.iter().any(|symbol| {
3236 symbol.kind == SymbolKind::Value
3237 && symbol.name == "value0"
3238 && symbol.detail.as_deref() == Some("vue-composition-binding")
3239 && symbol.parser == ParserKind::Structural
3240 }));
3241 }
3242
3243 #[test]
3244 fn vue_composition_binding_detection_requires_macro_call_boundary() {
3245 let source = r#"
3246<script setup lang="ts">
3247const data = refreshData();
3248const value = computedValue();
3249const typed = ref<string>("ok");
3250const delayed = computed (() => typed.value);
3251const props = withDefaults(defineProps<{ title: string }>(), { title: "Product" });
3252</script>
3253"#;
3254 let graph = extract_symbol_graph("src/Widget.vue", Some("vue"), source);
3255
3256 for absent in ["data", "value"] {
3257 assert!(
3258 graph.symbols.iter().all(|symbol| symbol.name != absent),
3259 "ordinary function call {absent} was incorrectly treated as a Vue binding"
3260 );
3261 }
3262 for expected in ["typed", "delayed", "props"] {
3263 assert!(
3264 graph.symbols.iter().any(|symbol| {
3265 symbol.kind == SymbolKind::Value
3266 && symbol.name == expected
3267 && symbol.detail.as_deref() == Some("vue-composition-binding")
3268 }),
3269 "missing Vue macro binding {expected}"
3270 );
3271 }
3272 }
3273
3274 #[test]
3275 fn extracts_python_docstrings() {
3276 let source = r#"
3277class Builder:
3278 """Builds atlas state."""
3279
3280 def build(self):
3281 """Build the atlas."""
3282 return "atlas"
3283"#;
3284 let graph = extract_symbol_graph("src/builder.py", Some("python"), source);
3285 assert!(graph.symbols.iter().any(|symbol| {
3286 symbol.kind == SymbolKind::Class
3287 && symbol.name == "Builder"
3288 && symbol.documentation.as_deref() == Some("Builds atlas state.")
3289 }));
3290 assert!(graph.symbols.iter().any(|symbol| {
3291 symbol.kind == SymbolKind::Method
3292 && symbol.name == "build"
3293 && symbol.documentation.as_deref() == Some("Build the atlas.")
3294 && symbol.parent.as_deref() == Some("Builder")
3295 }));
3296 }
3297
3298 #[test]
3299 fn extracts_java_package_classes_methods_and_calls() {
3300 let source = r"
3301package com.example.atlas;
3302
3303public class AtlasService {
3304 public void run() {
3305 helper();
3306 }
3307
3308 private void helper() {}
3309}
3310";
3311 let graph = extract_symbol_graph("src/AtlasService.java", Some("java"), source);
3312 assert!(graph.symbols.iter().any(|symbol| {
3313 symbol.kind == SymbolKind::Module && symbol.name == "com.example.atlas"
3314 }));
3315 assert!(graph.symbols.iter().any(|symbol| {
3316 symbol.kind == SymbolKind::Class && symbol.name == "AtlasService" && symbol.exported
3317 }));
3318 assert!(graph.symbols.iter().any(|symbol| {
3319 symbol.kind == SymbolKind::Method
3320 && symbol.name == "run"
3321 && symbol.parent.as_deref() == Some("AtlasService")
3322 && symbol.exported
3323 }));
3324 assert!(graph.relations.iter().any(|relation| {
3325 relation.kind == RelationKind::Calls && relation.target_name == "helper"
3326 }));
3327 }
3328
3329 #[test]
3330 fn extracts_go_package_functions_methods_and_imports() {
3331 let source = r#"
3332package atlas
3333
3334import "fmt"
3335
3336type Runner struct {}
3337
3338func (r Runner) Run() {
3339 helper()
3340}
3341
3342func helper() {
3343 fmt.Println("ok")
3344}
3345"#;
3346 let graph = extract_symbol_graph("service.go", Some("go"), source);
3347 assert!(
3348 graph
3349 .symbols
3350 .iter()
3351 .any(|symbol| { symbol.kind == SymbolKind::Module && symbol.name == "atlas" })
3352 );
3353 assert!(
3354 graph
3355 .symbols
3356 .iter()
3357 .any(|symbol| { symbol.kind == SymbolKind::Struct && symbol.name == "Runner" })
3358 );
3359 assert!(graph.symbols.iter().any(|symbol| {
3360 symbol.kind == SymbolKind::Method && symbol.name == "Run" && symbol.exported
3361 }));
3362 assert!(
3363 graph
3364 .symbols
3365 .iter()
3366 .any(|symbol| { symbol.kind == SymbolKind::Function && symbol.name == "helper" })
3367 );
3368 assert!(graph.relations.iter().any(|relation| {
3369 relation.kind == RelationKind::Imports && relation.target_name.contains("\"fmt\"")
3370 }));
3371 }
3372
3373 #[test]
3374 fn extracts_csharp_namespace_classes_and_methods() {
3375 let source = r"
3376namespace Atlas.Core;
3377
3378public class Runner
3379{
3380 public void Run()
3381 {
3382 Helper();
3383 }
3384
3385 private void Helper() {}
3386}
3387";
3388 let graph = extract_symbol_graph("Runner.cs", Some("csharp"), source);
3389 assert!(
3390 graph
3391 .symbols
3392 .iter()
3393 .any(|symbol| { symbol.kind == SymbolKind::Module && symbol.name == "Atlas.Core" })
3394 );
3395 assert!(graph.symbols.iter().any(|symbol| {
3396 symbol.kind == SymbolKind::Class && symbol.name == "Runner" && symbol.exported
3397 }));
3398 assert!(graph.symbols.iter().any(|symbol| {
3399 symbol.kind == SymbolKind::Method
3400 && symbol.name == "Run"
3401 && symbol.parent.as_deref() == Some("Runner")
3402 && symbol.exported
3403 }));
3404 }
3405
3406 #[test]
3407 fn extracts_remaining_specialized_language_basics() {
3408 let samples = [
3409 (
3410 "src/main.kt",
3411 "kotlin",
3412 r"
3413package com.example.atlas
3414
3415class Runner {
3416 fun run() {}
3417}
3418",
3419 SymbolKind::Class,
3420 "Runner",
3421 ),
3422 (
3423 "src/main.zig",
3424 "zig",
3425 r"
3426const Runner = struct {
3427 pub fn run(self: Runner) void {}
3428};
3429",
3430 SymbolKind::Function,
3431 "run",
3432 ),
3433 (
3434 "src/main.c",
3435 "c",
3436 r"
3437#include <stdio.h>
3438int run(void) { return 0; }
3439",
3440 SymbolKind::Function,
3441 "run",
3442 ),
3443 (
3444 "src/main.cpp",
3445 "cpp",
3446 r"
3447class Runner {
3448public:
3449 void run() {}
3450};
3451",
3452 SymbolKind::Class,
3453 "Runner",
3454 ),
3455 (
3456 "src/UserManager.m",
3457 "objective-c",
3458 r"
3459@interface UserManager
3460- (void)run;
3461@end
3462@implementation UserManager
3463- (void)run {}
3464@end
3465",
3466 SymbolKind::Class,
3467 "UserManager",
3468 ),
3469 ];
3470 for (path, language, source, kind, name) in samples {
3471 let graph = extract_symbol_graph(path, Some(language), source);
3472 assert!(
3473 graph
3474 .symbols
3475 .iter()
3476 .any(|symbol| symbol.kind == kind && symbol.name.contains(name)),
3477 "expected {language} sample to contain {kind:?} {name}, got {:?}",
3478 graph.symbols
3479 );
3480 }
3481 }
3482
3483 #[test]
3484 fn normalizes_language_specific_edge_summaries() {
3485 let kotlin = extract_symbol_graph(
3486 "src/KotlinRunner.kt",
3487 Some("kotlin"),
3488 r"
3489package com.example.atlas
3490class KotlinRunner { fun run() { helper() } private fun helper() {} }
3491",
3492 );
3493 assert!(kotlin.symbols.iter().any(|symbol| {
3494 symbol.kind == SymbolKind::Module && symbol.name == "com.example.atlas"
3495 }));
3496 assert!(
3497 kotlin.symbols.iter().any(|symbol| {
3498 symbol.kind == SymbolKind::Class && symbol.name == "KotlinRunner"
3499 })
3500 );
3501 assert!(kotlin.symbols.iter().any(|symbol| {
3502 symbol.kind == SymbolKind::Method
3503 && symbol.name == "run"
3504 && symbol.parent.as_deref() == Some("KotlinRunner")
3505 }));
3506
3507 for path in ["src/Worker.kt", "scripts/tasks.kts"] {
3508 let ordinary_kotlin = extract_symbol_graph(
3509 path,
3510 Some("kotlin"),
3511 r#"
3512class Worker {
3513 fun queue(tasks: TaskContainer) {
3514 tasks.register("notGradleTask")
3515 }
3516}
3517"#,
3518 );
3519 assert!(
3520 !ordinary_kotlin.symbols.iter().any(|symbol| {
3521 symbol.name == "notGradleTask"
3522 || symbol.detail.as_deref() == Some("gradle-kotlin-dsl-task")
3523 }),
3524 "ordinary Kotlin path {path} should not emit Gradle task symbols: {:?}",
3525 ordinary_kotlin.symbols
3526 );
3527 }
3528
3529 let gradle_kotlin = extract_symbol_graph(
3530 "build.gradle.kts",
3531 Some("kotlin"),
3532 r#"
3533import org.springframework.boot.gradle.tasks.run.BootRun
3534
3535fun loadDotEnv() = emptyMap<String, String>()
3536
3537tasks.register<BootRun>("bootRunE2E") {
3538 group = "verification"
3539}
3540
3541val verifyAtlas by tasks.registering {
3542 group = "verification"
3543}
3544
3545tasks {
3546 register<Copy>("copyE2EReports") {
3547 group = "verification"
3548 }
3549}
3550
3551task("publishKtsE2E") {}
3552"#,
3553 );
3554 assert_eq!(gradle_kotlin.parser, ParserKind::TreeSitter);
3555 for task in [
3556 "bootRunE2E",
3557 "copyE2EReports",
3558 "verifyAtlas",
3559 "publishKtsE2E",
3560 ] {
3561 assert!(gradle_kotlin.symbols.iter().any(|symbol| {
3562 symbol.kind == SymbolKind::Function
3563 && symbol.name == task
3564 && symbol.detail.as_deref() == Some("gradle-kotlin-dsl-task")
3565 }));
3566 }
3567 let fallback_gradle_kotlin = extract_fallback_graph(
3568 "build.gradle.kts",
3569 Some("kotlin"),
3570 r#"
3571tasks.register<BootRun>("bootRunE2E") {
3572 group = "verification"
3573}
3574
3575fun broken(
3576"#,
3577 );
3578 assert_eq!(fallback_gradle_kotlin.parser, ParserKind::Fallback);
3579 assert!(
3580 fallback_gradle_kotlin.symbols.iter().any(|symbol| {
3581 symbol.kind == SymbolKind::Function
3582 && symbol.name == "bootRunE2E"
3583 && symbol.detail.as_deref() == Some("gradle-kotlin-dsl-task")
3584 }),
3585 "fallback Gradle KTS graph should retain task symbols: {:?}",
3586 fallback_gradle_kotlin.symbols
3587 );
3588
3589 let gradle_groovy = extract_symbol_graph(
3590 "build.gradle",
3591 Some("groovy"),
3592 r"
3593plugins { id 'java' }
3594
3595tasks.register('bootRunSmoke', BootRun) {
3596 group = 'verification'
3597}
3598
3599task cleanE2E(type: Delete) {}
3600
3601tasks {
3602 create('copyGroovyReports') {
3603 group = 'verification'
3604 }
3605}
3606
3607task('publishE2E') {}
3608",
3609 );
3610 assert_eq!(gradle_groovy.parser, ParserKind::Fallback);
3611 for task in [
3612 "bootRunSmoke",
3613 "cleanE2E",
3614 "copyGroovyReports",
3615 "publishE2E",
3616 ] {
3617 assert!(gradle_groovy.symbols.iter().any(|symbol| {
3618 symbol.kind == SymbolKind::Function
3619 && symbol.name == task
3620 && symbol.detail.as_deref() == Some("gradle-groovy-dsl-task")
3621 }));
3622 }
3623
3624 let zig = extract_symbol_graph(
3625 "src/runner.zig",
3626 Some("zig"),
3627 "const ZigRunner = struct { pub fn run(self: ZigRunner) void {} };\n",
3628 );
3629 assert!(
3630 zig.symbols
3631 .iter()
3632 .any(|symbol| { symbol.kind == SymbolKind::Struct && symbol.name == "ZigRunner" })
3633 );
3634 assert!(zig.symbols.iter().any(|symbol| {
3635 symbol.kind == SymbolKind::Method
3636 && symbol.name == "run"
3637 && symbol.parent.as_deref() == Some("ZigRunner")
3638 }));
3639 assert!(
3640 !zig.symbols
3641 .iter()
3642 .any(|symbol| symbol.name.contains("struct {"))
3643 );
3644
3645 let c_graph = extract_symbol_graph(
3646 "src/runner.c",
3647 Some("c"),
3648 "#include <stdio.h>\nint c_run(void) { return 0; }\n",
3649 );
3650 let c_run_count = c_graph
3651 .symbols
3652 .iter()
3653 .filter(|symbol| symbol.kind == SymbolKind::Function && symbol.name == "c_run")
3654 .count();
3655 assert_eq!(c_run_count, 1);
3656 assert!(
3657 c_graph
3658 .symbols
3659 .iter()
3660 .all(|symbol| symbol.documentation.as_deref() != Some("include <stdio.h>"))
3661 );
3662
3663 let cpp_graph = extract_symbol_graph(
3664 "src/runner.cpp",
3665 Some("cpp"),
3666 "class CppRunner { public: void run(); void inline_run() {} };\n",
3667 );
3668 let cpp_run_names = cpp_graph
3669 .symbols
3670 .iter()
3671 .filter(|symbol| symbol.parent.as_deref() == Some("CppRunner"))
3672 .map(|symbol| symbol.name.as_str())
3673 .collect::<Vec<_>>();
3674 assert_eq!(cpp_run_names, vec!["run", "inline_run"]);
3675 assert!(cpp_graph.symbols.iter().all(|symbol| {
3676 symbol.parent.as_deref() != Some("CppRunner") || symbol.kind == SymbolKind::Method
3677 }));
3678
3679 let objc_graph = extract_symbol_graph(
3680 "src/ObjRunner.m",
3681 Some("objective-c"),
3682 r"
3683@interface ObjRunner
3684- (void)run;
3685@end
3686@implementation ObjRunner
3687- (void)run {}
3688@end
3689",
3690 );
3691 assert_eq!(
3692 objc_graph
3693 .symbols
3694 .iter()
3695 .filter(|symbol| symbol.kind == SymbolKind::Class && symbol.name == "ObjRunner")
3696 .count(),
3697 1
3698 );
3699 assert_eq!(
3700 objc_graph
3701 .symbols
3702 .iter()
3703 .filter(|symbol| symbol.kind == SymbolKind::Method && symbol.name == "run")
3704 .count(),
3705 1
3706 );
3707 assert!(
3708 !objc_graph
3709 .symbols
3710 .iter()
3711 .any(|symbol| symbol.kind == SymbolKind::Function && symbol.name == "run")
3712 );
3713 assert!(objc_graph.symbols.iter().any(|symbol| {
3714 symbol.kind == SymbolKind::Method
3715 && symbol.name == "run"
3716 && symbol.signature.contains("run")
3717 && !symbol.signature.contains('{')
3718 }));
3719 }
3720
3721 #[test]
3722 fn extracts_cargo_manifest_symbols() {
3723 let source = r#"
3724[package]
3725name = "projectatlas"
3726
3727[dependencies]
3728tree-sitter = "0.26"
3729serde_json = { workspace = true }
3730serde_alias = { version = "1", package = "serde" }
3731
3732[target.'cfg(windows)'.dependencies]
3733windows-sys = "0.60"
3734"#;
3735 let graph = extract_symbol_graph("Cargo.toml", Some("cargo-manifest"), source);
3736 assert!(
3737 graph.symbols.iter().any(|symbol| {
3738 symbol.kind == SymbolKind::Package && symbol.name == "projectatlas"
3739 })
3740 );
3741 assert!(graph.symbols.iter().any(|symbol| {
3742 symbol.kind == SymbolKind::Dependency && symbol.name == "tree-sitter"
3743 }));
3744 assert!(
3745 graph
3746 .symbols
3747 .iter()
3748 .any(|symbol| { symbol.kind == SymbolKind::Dependency && symbol.name == "serde" })
3749 );
3750 assert!(graph.symbols.iter().any(|symbol| {
3751 symbol.kind == SymbolKind::Dependency && symbol.name == "windows-sys"
3752 }));
3753 }
3754
3755 #[test]
3756 fn cargo_lock_duplicate_package_names_keep_distinct_lines() {
3757 let source = r#"[[package]]
3758name = "windows-sys"
3759version = "0.59.0"
3760
3761[[package]]
3762name = "windows-sys"
3763version = "0.60.0"
3764"#;
3765 let graph = extract_symbol_graph("Cargo.lock", Some("cargo-lock"), source);
3766 let lines = graph
3767 .symbols
3768 .iter()
3769 .filter(|symbol| symbol.kind == SymbolKind::Dependency && symbol.name == "windows-sys")
3770 .map(|symbol| symbol.line_start)
3771 .collect::<Vec<_>>();
3772 assert_eq!(lines, vec![2, 6]);
3773 }
3774
3775 #[test]
3776 fn specialized_language_registry_covers_target_set() {
3777 for expected in [
3778 "rust",
3779 "python",
3780 "javascript",
3781 "typescript",
3782 "java",
3783 "kotlin",
3784 "csharp",
3785 "go",
3786 "objective-c",
3787 "zig",
3788 ] {
3789 assert!(specialized_languages().contains(&expected));
3790 }
3791 }
3792}