1use std::collections::HashSet;
5use std::path::Path;
6
7use streaming_iterator::StreamingIterator;
8use tree_sitter::{Parser, Query, QueryCursor};
9
10use crate::index::hasher::symbol_content_hash;
11use crate::index::import_resolution::{self, ExtractedImports, ImportBindings};
12use crate::index::languages;
13use crate::index::security;
14use crate::index::semantic::{SemanticCallRequest, SemanticCallResolver};
15use crate::models::{CallRelation, ParseResult, Symbol};
16
17pub use crate::index::import_resolution::{
18 ImportResolutionContext, build_import_resolution_context,
19};
20
21const MAX_FILE_SIZE: u64 = 10 * 1024 * 1024;
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25enum CallSyntaxKind {
26 Bare,
27 Member,
28 Other,
29}
30
31pub(crate) fn parse_file_with_semantic(
32 file_path: &Path,
33 project_id: &str,
34 root_path: &Path,
35 exclude_patterns: &[String],
36 import_context: &ImportResolutionContext,
37 semantic_resolver: Option<&mut (dyn SemanticCallResolver + '_)>,
38) -> anyhow::Result<Option<ParseResult>> {
39 if !security::validate_path(file_path, root_path) {
41 return Ok(None);
42 }
43 if !security::is_symlink_safe(file_path, root_path) {
44 return Ok(None);
45 }
46 if security::should_exclude_path(root_path, file_path, exclude_patterns) {
47 return Ok(None);
48 }
49 if security::has_secret_extension(file_path) {
50 return Ok(None);
51 }
52
53 let Ok(meta) = file_path.metadata() else {
54 return Ok(None);
55 };
56 if meta.len() == 0 || meta.len() > MAX_FILE_SIZE {
57 return Ok(None);
58 }
59
60 if security::is_binary(file_path) {
61 return Ok(None);
62 }
63
64 let file_str = file_path.to_string_lossy();
65 let Some(language) = languages::detect_language(&file_str) else {
66 return Ok(None);
67 };
68 let Some(spec) = languages::get_spec(language) else {
69 return Ok(None);
70 };
71 let Some(ts_lang) = languages::get_ts_language(language) else {
72 return Ok(None);
73 };
74
75 let Ok(source) = std::fs::read(file_path) else {
76 return Ok(None);
77 };
78
79 let mut parser = Parser::new();
80 if parser.set_language(&ts_lang).is_err() {
81 return Ok(None);
82 }
83 let Some(tree) = parser.parse(&source, None) else {
84 return Ok(None);
85 };
86
87 let rel_path = file_path
88 .canonicalize()
89 .ok()
90 .and_then(|abs| {
91 root_path.canonicalize().ok().and_then(|root| {
92 abs.strip_prefix(&root)
93 .ok()
94 .map(|p| p.to_string_lossy().to_string())
95 })
96 })
97 .unwrap_or_else(|| file_str.to_string());
98
99 let mut symbols = extract_symbols(
100 &tree, &source, spec, language, &ts_lang, project_id, &rel_path,
101 );
102 link_parents(&mut symbols);
103 let extracted_imports = extract_imports(
104 &tree,
105 &source,
106 spec,
107 language,
108 &ts_lang,
109 &rel_path,
110 import_context,
111 );
112 let calls = extract_calls(
113 &tree,
114 &source,
115 spec,
116 CallExtractionContext {
117 language,
118 ts_lang: &ts_lang,
119 rel_path: &rel_path,
120 symbols: &symbols,
121 import_context,
122 import_bindings: &extracted_imports.bindings,
123 file_path,
124 root_path,
125 },
126 semantic_resolver,
127 )?;
128
129 Ok(Some(ParseResult {
130 symbols,
131 imports: extracted_imports.imports,
132 calls,
133 source,
134 }))
135}
136
137fn extract_symbols(
138 tree: &tree_sitter::Tree,
139 source: &[u8],
140 spec: &languages::LanguageSpec,
141 language: &str,
142 ts_lang: &tree_sitter::Language,
143 project_id: &str,
144 rel_path: &str,
145) -> Vec<Symbol> {
146 if spec.symbol_query.trim().is_empty() {
147 return Vec::new();
148 }
149
150 let query = match Query::new(ts_lang, spec.symbol_query) {
151 Ok(q) => q,
152 Err(_) => return Vec::new(),
153 };
154
155 let mut cursor = QueryCursor::new();
156 let mut matches = cursor.matches(&query, tree.root_node(), source);
157
158 let mut symbols = Vec::new();
159 let mut seen_ids = HashSet::new();
160 let capture_names: Vec<String> = query
161 .capture_names()
162 .iter()
163 .map(|s| s.to_string())
164 .collect();
165
166 while let Some(m) = matches.next() {
167 let mut name_text: Option<String> = None;
168 let mut def_node = None;
169 let mut kind = String::from("function");
170
171 for cap in m.captures {
172 let cap_name = &capture_names[cap.index as usize];
173 if cap_name == "name" {
174 name_text = Some(
175 String::from_utf8_lossy(&source[cap.node.start_byte()..cap.node.end_byte()])
176 .to_string(),
177 );
178 } else if let Some(k) = cap_name.strip_prefix("definition.") {
179 def_node = Some(cap.node);
180 kind = k.to_string();
181 }
182 }
183
184 let (name, node) = match (name_text, def_node) {
185 (Some(n), Some(d)) => (n, d),
186 _ => continue,
187 };
188
189 let sig_end = source[node.start_byte()..]
191 .iter()
192 .position(|&b| b == b'\n')
193 .map(|p| node.start_byte() + p)
194 .unwrap_or(node.end_byte());
195 let mut signature = String::from_utf8_lossy(&source[node.start_byte()..sig_end])
196 .trim()
197 .to_string();
198 if signature.len() > 200 {
199 signature.truncate(200);
200 signature.push_str("...");
201 }
202
203 let docstring = extract_docstring(&node, source, language);
204 let c_hash =
205 symbol_content_hash(source, node.start_byte(), node.end_byte()).unwrap_or_default();
206 let symbol_id = Symbol::make_id(project_id, rel_path, &name, &kind, node.start_byte());
207
208 if seen_ids.contains(&symbol_id) {
209 continue;
210 }
211 seen_ids.insert(symbol_id.clone());
212
213 symbols.push(Symbol {
214 id: symbol_id,
215 project_id: project_id.to_string(),
216 file_path: rel_path.to_string(),
217 name: name.clone(),
218 qualified_name: name,
219 kind,
220 language: language.to_string(),
221 byte_start: node.start_byte(),
222 byte_end: node.end_byte(),
223 line_start: node.start_position().row + 1,
224 line_end: node.end_position().row + 1,
225 signature: Some(signature),
226 docstring,
227 parent_symbol_id: None,
228 content_hash: c_hash,
229 summary: None,
230 created_at: String::new(),
231 updated_at: String::new(),
232 });
233 }
234
235 symbols
236}
237
238fn link_parents(symbols: &mut [Symbol]) {
239 let mut indices: Vec<usize> = (0..symbols.len()).collect();
240 indices.sort_by_key(|&i| symbols[i].byte_start);
241
242 for idx in 0..indices.len() {
243 let i = indices[idx];
244 for jdx in (0..idx).rev() {
245 let j = indices[jdx];
246 let parent_kind = symbols[j].kind.as_str();
247 if (parent_kind == "class" || parent_kind == "type")
248 && symbols[j].byte_start <= symbols[i].byte_start
249 && symbols[j].byte_end >= symbols[i].byte_end
250 {
251 let parent_name = symbols[j].name.clone();
252 let parent_id = symbols[j].id.clone();
253 let sym = &mut symbols[i];
254 sym.parent_symbol_id = Some(parent_id);
255 sym.qualified_name = format!("{}.{}", parent_name, sym.name);
256 if sym.kind == "function" {
257 sym.kind = "method".to_string();
258 }
259 break;
260 }
261 }
262 }
263}
264
265fn extract_docstring(node: &tree_sitter::Node, source: &[u8], language: &str) -> Option<String> {
266 if !matches!(language, "python" | "javascript" | "typescript") {
267 return None;
268 }
269
270 let mut body = None;
271 let mut walk = node.walk();
272 for child in node.children(&mut walk) {
273 let ty = child.kind();
274 if ty == "block" || ty == "statement_block" {
275 body = Some(child);
276 break;
277 }
278 }
279 let body = body?;
280
281 let mut walk2 = body.walk();
282 for child in body.children(&mut walk2) {
283 let ty = child.kind();
284 if ty == "comment" || ty == "\n" || ty == "newline" {
285 continue;
286 }
287
288 let string_node = if ty == "string" {
289 Some(child)
290 } else if ty == "expression_statement" {
291 let mut w3 = child.walk();
292 child.children(&mut w3).find(|gc| gc.kind() == "string")
293 } else {
294 None
295 };
296
297 let string_node = string_node?;
298
299 let mut w4 = string_node.walk();
301 for sc in string_node.children(&mut w4) {
302 if sc.kind() == "string_content" {
303 let raw = String::from_utf8_lossy(&source[sc.start_byte()..sc.end_byte()]);
304 let trimmed = raw.trim();
305 return if trimmed.is_empty() {
306 None
307 } else {
308 Some(trimmed.to_string())
309 };
310 }
311 }
312
313 let raw =
315 String::from_utf8_lossy(&source[string_node.start_byte()..string_node.end_byte()]);
316 let raw = raw.trim();
317 let stripped = strip_quotes(raw);
318 return if stripped.is_empty() {
319 None
320 } else {
321 Some(stripped.to_string())
322 };
323 }
324
325 None
326}
327
328fn strip_quotes(s: &str) -> &str {
329 for q in &["\"\"\"", "'''", "\"", "'"] {
330 if s.starts_with(q) && s.ends_with(q) && s.len() >= q.len() * 2 {
331 return s[q.len()..s.len() - q.len()].trim();
332 }
333 }
334 s
335}
336
337fn extract_imports(
338 tree: &tree_sitter::Tree,
339 source: &[u8],
340 spec: &languages::LanguageSpec,
341 language: &str,
342 ts_lang: &tree_sitter::Language,
343 rel_path: &str,
344 import_context: &ImportResolutionContext,
345) -> ExtractedImports {
346 if spec.import_query.trim().is_empty() {
347 return ExtractedImports::default();
348 }
349
350 let query = match Query::new(ts_lang, spec.import_query) {
351 Ok(q) => q,
352 Err(_) => return ExtractedImports::default(),
353 };
354
355 let mut cursor = QueryCursor::new();
356 let mut matches = cursor.matches(&query, tree.root_node(), source);
357 let capture_names: Vec<String> = query
358 .capture_names()
359 .iter()
360 .map(|s| s.to_string())
361 .collect();
362 let mut extracted = ExtractedImports::default();
363
364 while let Some(m) = matches.next() {
365 for cap in m.captures {
366 let cap_name = &capture_names[cap.index as usize];
367 if cap_name == "import" {
368 let text =
369 String::from_utf8_lossy(&source[cap.node.start_byte()..cap.node.end_byte()])
370 .trim()
371 .to_string();
372 import_resolution::parse_import_statement(
373 language,
374 &text,
375 rel_path,
376 import_context,
377 &mut extracted,
378 );
379 }
380 }
381 }
382
383 import_resolution::seed_import_bindings(language, import_context, &mut extracted.bindings);
384 extracted
385}
386
387struct CallExtractionContext<'a> {
388 language: &'a str,
389 ts_lang: &'a tree_sitter::Language,
390 rel_path: &'a str,
391 symbols: &'a [Symbol],
392 import_context: &'a ImportResolutionContext,
393 import_bindings: &'a ImportBindings,
394 file_path: &'a Path,
395 root_path: &'a Path,
396}
397
398fn extract_calls(
399 tree: &tree_sitter::Tree,
400 source: &[u8],
401 spec: &languages::LanguageSpec,
402 ctx: CallExtractionContext<'_>,
403 mut semantic_resolver: Option<&mut (dyn SemanticCallResolver + '_)>,
404) -> anyhow::Result<Vec<CallRelation>> {
405 let language = ctx.language;
406 let rel_path = ctx.rel_path;
407 let symbols = ctx.symbols;
408 let import_context = ctx.import_context;
409 let import_bindings = ctx.import_bindings;
410 if language == "dart" {
411 return extract_textual_dart_calls(source, ctx, semantic_resolver);
412 }
413 if spec.call_query.trim().is_empty() {
414 return Ok(Vec::new());
415 }
416
417 let query = match Query::new(ctx.ts_lang, spec.call_query) {
418 Ok(q) => q,
419 Err(_) => return Ok(Vec::new()),
420 };
421
422 let mut cursor = QueryCursor::new();
423 let mut matches = cursor.matches(&query, tree.root_node(), source);
424 let capture_names: Vec<String> = query
425 .capture_names()
426 .iter()
427 .map(|s| s.to_string())
428 .collect();
429 let mut calls = Vec::new();
430
431 while let Some(m) = matches.next() {
432 let mut name_node = None;
433 let mut call_node = None;
434
435 for cap in m.captures {
436 let cap_name = &capture_names[cap.index as usize];
437 if cap_name == "name" {
438 name_node = Some(cap.node);
439 } else if cap_name == "call" {
440 call_node = Some(cap.node);
441 }
442 }
443
444 let name_n = match name_node {
445 Some(n) => n,
446 None => continue,
447 };
448
449 let raw_callee =
450 String::from_utf8_lossy(&source[name_n.start_byte()..name_n.end_byte()]).to_string();
451 let (callee_name, qualifier_from_name) = split_qualified_callee(&raw_callee);
452 if should_ignore_call_name(language, &callee_name) {
453 continue;
454 }
455
456 let target = call_node.unwrap_or(name_n);
457 let caller_symbol = enclosing_symbol(symbols, target.start_byte());
458 let caller_symbol_id = caller_symbol.map(|s| s.id.clone()).unwrap_or_default();
459 let qualifier_path = call_qualifier_path(qualifier_from_name, || {
462 member_qualifier_path(source, target, name_n)
463 });
464 let detected_syntax = call_syntax_kind(name_n, target);
465 let syntax = if detected_syntax == CallSyntaxKind::Bare && qualifier_path.is_some() {
466 CallSyntaxKind::Member
467 } else {
468 detected_syntax
469 };
470 let local_target = resolve_same_file_callee_for_language(
471 language,
472 symbols,
473 caller_symbol,
474 &callee_name,
475 syntax,
476 );
477 let root_alias = qualifier_path
478 .as_deref()
479 .and_then(qualifier_root_alias)
480 .map(ToOwned::to_owned);
481 let external_shadowed = external_call_is_shadowed(
482 source,
483 caller_symbol,
484 target.start_byte(),
485 &callee_name,
486 root_alias.as_deref(),
487 syntax,
488 );
489 let external_target = if external_shadowed {
490 None
491 } else {
492 import_resolution::resolve_external_callee(
493 import_context,
494 import_bindings,
495 symbols,
496 &callee_name,
497 root_alias.as_deref(),
498 qualifier_path.as_deref(),
499 syntax == CallSyntaxKind::Bare,
500 )
501 };
502 let semantic_target =
503 if local_target.is_none() && external_target.is_none() && !external_shadowed {
504 if let Some(resolver) = semantic_resolver.as_deref_mut() {
505 resolver.resolve(&SemanticCallRequest {
506 language,
507 file_path: ctx.file_path,
508 root_path: ctx.root_path,
509 source,
510 callee_name: &callee_name,
511 line: name_n.start_position().row + 1,
512 column: utf16_column_at_byte(source, name_n.start_byte()),
513 })?
514 } else {
515 None
516 }
517 } else {
518 None
519 };
520
521 let mut call = CallRelation::new(
522 caller_symbol_id,
523 callee_name,
524 rel_path.to_string(),
525 name_n.start_position().row + 1,
526 );
527 match (local_target, external_target) {
528 (Some(callee_symbol_id), None) => {
529 call = call.with_symbol_target(callee_symbol_id);
530 }
531 (None, Some(external_target)) => {
532 call =
533 call.with_external_target(external_target.callee_name, external_target.module);
534 }
535 (None, None) => {
536 if let Some(semantic_target) = semantic_target {
537 call = call.with_external_target(
538 semantic_target.callee_name,
539 semantic_target.external_module,
540 );
541 }
542 }
543 _ => {}
544 }
545 calls.push(call);
546 }
547
548 Ok(calls)
549}
550
551fn extract_textual_dart_calls(
552 source: &[u8],
553 ctx: CallExtractionContext<'_>,
554 mut semantic_resolver: Option<&mut (dyn SemanticCallResolver + '_)>,
555) -> anyhow::Result<Vec<CallRelation>> {
556 let rel_path = ctx.rel_path;
557 let symbols = ctx.symbols;
558 let import_context = ctx.import_context;
559 let import_bindings = ctx.import_bindings;
560 let file_path = ctx.file_path;
561 let root_path = ctx.root_path;
562 let text = String::from_utf8_lossy(source);
563 let mut calls = Vec::new();
564 let mut line_start_byte = 0usize;
565 let mut dart_state = DartScanState::default();
566
567 for (row, line) in text.lines().enumerate() {
568 let terminator_len = line_terminator_len(&text, line_start_byte, line.len());
569 let trimmed = line.trim_start();
570 if dart_state.is_code()
571 && (trimmed.starts_with("import ")
572 || trimmed.starts_with("export ")
573 || trimmed.starts_with("class ")
574 || trimmed.starts_with("enum ")
575 || trimmed.starts_with("typedef "))
576 {
577 dart_state = dart_state_after_line(line, dart_state);
578 line_start_byte += line.len() + terminator_len;
579 continue;
580 }
581
582 for candidate in textual_call_candidates(line, line_start_byte, &['.']) {
583 let candidate_line_byte = candidate.call_byte.saturating_sub(line_start_byte);
584 if dart_textual_candidate_in_ignored_context(line, candidate_line_byte, dart_state) {
585 continue;
586 }
587 if should_ignore_call_name("dart", &candidate.name) {
588 continue;
589 }
590 let caller_symbol = enclosing_symbol(symbols, candidate.call_byte);
591 let caller_symbol_id = caller_symbol.map(|s| s.id.clone()).unwrap_or_default();
592 let syntax = if candidate.qualifier_path.is_some() {
593 CallSyntaxKind::Member
594 } else {
595 CallSyntaxKind::Bare
596 };
597 let local_target = resolve_same_file_callee_for_language(
598 "dart",
599 symbols,
600 caller_symbol,
601 &candidate.name,
602 syntax,
603 );
604 let root_alias = candidate
605 .qualifier_path
606 .as_deref()
607 .and_then(qualifier_root_alias)
608 .map(ToOwned::to_owned);
609 let external_shadowed = external_call_is_shadowed(
610 source,
611 caller_symbol,
612 candidate.call_byte,
613 &candidate.name,
614 root_alias.as_deref(),
615 syntax,
616 );
617 let external_target = if external_shadowed {
618 None
619 } else {
620 import_resolution::resolve_external_callee(
621 import_context,
622 import_bindings,
623 symbols,
624 &candidate.name,
625 root_alias.as_deref(),
626 candidate.qualifier_path.as_deref(),
627 syntax == CallSyntaxKind::Bare,
628 )
629 };
630 let semantic_target =
631 if local_target.is_none() && external_target.is_none() && !external_shadowed {
632 if let Some(resolver) = semantic_resolver.as_deref_mut() {
633 resolver.resolve(&SemanticCallRequest {
634 language: "dart",
635 file_path,
636 root_path,
637 source,
638 callee_name: &candidate.name,
639 line: row + 1,
640 column: utf16_column_at_byte(source, candidate.call_byte),
641 })?
642 } else {
643 None
644 }
645 } else {
646 None
647 };
648
649 let mut call = CallRelation::new(
650 caller_symbol_id,
651 candidate.name,
652 rel_path.to_string(),
653 row + 1,
654 );
655 match (local_target, external_target) {
656 (Some(callee_symbol_id), None) => {
657 call = call.with_symbol_target(callee_symbol_id);
658 }
659 (None, Some(external_target)) => {
660 call = call
661 .with_external_target(external_target.callee_name, external_target.module);
662 }
663 (None, None) => {
664 if let Some(semantic_target) = semantic_target {
665 call = call.with_external_target(
666 semantic_target.callee_name,
667 semantic_target.external_module,
668 );
669 }
670 }
671 _ => {}
672 }
673 calls.push(call);
674 }
675
676 dart_state = dart_state_after_line(line, dart_state);
677 line_start_byte += line.len() + terminator_len;
678 }
679
680 Ok(calls)
681}
682
683fn line_terminator_len(text: &str, line_start_byte: usize, line_len: usize) -> usize {
684 let terminator_start = line_start_byte + line_len;
685 let Some(rest) = text.as_bytes().get(terminator_start..) else {
686 return 0;
687 };
688 if rest.starts_with(b"\r\n") {
689 2
690 } else if rest.starts_with(b"\n") {
691 1
692 } else {
693 0
694 }
695}
696
697fn utf16_column_at_byte(source: &[u8], byte_offset: usize) -> usize {
698 let byte_offset = byte_offset.min(source.len());
699 let line_start = source[..byte_offset]
700 .iter()
701 .rposition(|byte| *byte == b'\n')
702 .map(|idx| idx + 1)
703 .unwrap_or(0);
704 String::from_utf8_lossy(&source[line_start..byte_offset])
705 .encode_utf16()
706 .count()
707}
708
709#[derive(Debug)]
710struct TextualCallCandidate {
711 name: String,
712 qualifier_path: Option<String>,
713 call_byte: usize,
714}
715
716fn textual_call_candidates(
717 line: &str,
718 line_start_byte: usize,
719 separators: &[char],
720) -> Vec<TextualCallCandidate> {
721 let bytes = line.as_bytes();
722 let mut candidates = Vec::new();
723 let mut idx = 0usize;
724
725 while idx < bytes.len() {
726 if bytes[idx] != b'(' {
727 idx += 1;
728 continue;
729 }
730 let mut end = idx;
731 while end > 0 && bytes[end - 1].is_ascii_whitespace() {
732 end -= 1;
733 }
734 let (start, name_end) = if end > 0 && bytes[end - 1] == b'>' {
735 let Some(generic_start) = matching_angle_start(bytes, end - 1) else {
736 idx += 1;
737 continue;
738 };
739 let mut start = generic_start;
740 while start > 0 && is_textual_call_name_byte(bytes[start - 1]) {
741 start -= 1;
742 }
743 (start, generic_start)
744 } else {
745 let mut start = end;
746 while start > 0 && is_textual_call_name_byte(bytes[start - 1]) {
747 start -= 1;
748 }
749 (start, end)
750 };
751 if start == end {
752 idx += 1;
753 continue;
754 }
755
756 let name = &line[start..name_end];
757 if name.is_empty() {
758 idx += 1;
759 continue;
760 }
761 if looks_like_textual_function_declaration(line, start, idx) {
762 idx += 1;
763 continue;
764 }
765 let mut qualifier_path = None;
766 let mut prefix_end = start;
767 while prefix_end > 0 && bytes[prefix_end - 1].is_ascii_whitespace() {
768 prefix_end -= 1;
769 }
770 if prefix_end > 0 && separators.contains(&(bytes[prefix_end - 1] as char)) {
771 let mut qualifier_start = prefix_end - 1;
772 while qualifier_start > 0 {
773 let ch = bytes[qualifier_start - 1] as char;
774 if is_identifier_continue(ch) || separators.contains(&ch) {
775 qualifier_start -= 1;
776 } else {
777 break;
778 }
779 }
780 let qualifier = line[qualifier_start..prefix_end - 1].trim();
781 if !qualifier.is_empty() {
782 qualifier_path = Some(qualifier.to_string());
783 }
784 }
785
786 candidates.push(TextualCallCandidate {
787 name: name.to_string(),
788 qualifier_path,
789 call_byte: line_start_byte + start,
790 });
791 idx += 1;
792 }
793
794 candidates
795}
796
797fn matching_angle_start(bytes: &[u8], close_idx: usize) -> Option<usize> {
798 let mut depth = 0usize;
799 for idx in (0..=close_idx).rev() {
800 match bytes[idx] {
801 b'>' => depth += 1,
802 b'<' if depth > 0 => {
803 depth -= 1;
804 if depth == 0 {
805 return Some(idx);
806 }
807 }
808 _ => {}
809 }
810 }
811 None
812}
813
814#[derive(Debug, Clone, Copy, Default)]
815struct DartScanState {
816 in_block_comment: bool,
817 string: Option<DartStringState>,
818}
819
820impl DartScanState {
821 fn is_code(self) -> bool {
822 !self.in_block_comment && self.string.is_none()
823 }
824}
825
826#[derive(Debug, Clone, Copy)]
827struct DartStringState {
828 quote: u8,
829 triple: bool,
830 raw: bool,
831 escaped: bool,
832}
833
834fn dart_textual_candidate_in_ignored_context(
835 line: &str,
836 candidate_byte: usize,
837 state: DartScanState,
838) -> bool {
839 let (state, in_line_comment) = dart_scan_line_until(line, candidate_byte, state);
840 in_line_comment || !state.is_code()
841}
842
843fn dart_state_after_line(line: &str, state: DartScanState) -> DartScanState {
844 dart_scan_line_until(line, line.len(), state).0
845}
846
847fn dart_scan_line_until(
848 line: &str,
849 limit: usize,
850 mut state: DartScanState,
851) -> (DartScanState, bool) {
852 let bytes = line.as_bytes();
853 let limit = limit.min(bytes.len());
854 let mut idx = 0usize;
855
856 while idx < limit {
857 if state.in_block_comment {
858 if bytes[idx..].starts_with(b"*/") {
859 state.in_block_comment = false;
860 idx += 2;
861 } else {
862 idx += 1;
863 }
864 continue;
865 }
866
867 if let Some(mut string) = state.string {
868 if string.triple
869 && bytes[idx..].starts_with(&[string.quote, string.quote, string.quote])
870 {
871 state.string = None;
872 idx += 3;
873 continue;
874 }
875 if !string.triple {
876 if !string.raw && string.escaped {
877 string.escaped = false;
878 } else if !string.raw && bytes[idx] == b'\\' {
879 string.escaped = true;
880 } else if bytes[idx] == string.quote {
881 state.string = None;
882 idx += 1;
883 continue;
884 }
885 state.string = Some(string);
886 }
887 idx += 1;
888 continue;
889 }
890
891 if bytes[idx..].starts_with(b"//") {
892 return (state, true);
893 }
894 if bytes[idx..].starts_with(b"/*") {
895 state.in_block_comment = true;
896 idx += 2;
897 continue;
898 }
899 if let Some((string, consumed)) = dart_string_start(bytes, idx) {
900 state.string = Some(string);
901 idx += consumed;
902 continue;
903 }
904 idx += 1;
905 }
906
907 (state, false)
908}
909
910fn dart_string_start(bytes: &[u8], idx: usize) -> Option<(DartStringState, usize)> {
911 let (raw, quote_idx) =
912 if bytes.get(idx) == Some(&b'r') && matches!(bytes.get(idx + 1), Some(b'\'' | b'"')) {
913 (true, idx + 1)
914 } else if matches!(bytes.get(idx), Some(b'\'' | b'"')) {
915 (false, idx)
916 } else {
917 return None;
918 };
919 let quote = bytes[quote_idx];
920 let triple = bytes
921 .get(quote_idx..quote_idx + 3)
922 .is_some_and(|slice| slice == [quote, quote, quote]);
923 Some((
924 DartStringState {
925 quote,
926 triple,
927 raw,
928 escaped: false,
929 },
930 (if raw { 1 } else { 0 }) + if triple { 3 } else { 1 },
931 ))
932}
933
934fn looks_like_textual_function_declaration(
935 line: &str,
936 name_start: usize,
937 open_paren: usize,
938) -> bool {
939 let prefix = line[..name_start].trim_end();
940 let after_paren = &line[open_paren + 1..];
941 let after_args = after_paren
942 .find(')')
943 .and_then(|close| after_paren.get(close + 1..))
944 .unwrap_or_default()
945 .trim_start();
946 let has_declaration_tail = after_args.starts_with('{')
947 || after_args.starts_with("=>")
948 || after_args.starts_with("async")
949 || after_args.starts_with("sync")
950 || after_args.starts_with("external")
951 || after_args.starts_with(';');
952 if !has_declaration_tail {
953 return false;
954 }
955
956 if prefix.is_empty() {
957 return !after_args.starts_with(';');
958 }
959 if prefix.contains(['=', '.', '(', ',', ';']) {
960 return false;
961 }
962 let Some(previous_token) = prefix.split_whitespace().last() else {
963 return false;
964 };
965 previous_token.contains('<')
966 || previous_token
967 .chars()
968 .next()
969 .is_some_and(|ch| ch.is_ascii_uppercase())
970 || matches!(
971 previous_token,
972 "void"
973 | "int"
974 | "double"
975 | "num"
976 | "String"
977 | "bool"
978 | "dynamic"
979 | "Object"
980 | "Future"
981 | "Stream"
982 )
983}
984
985fn enclosing_symbol(symbols: &[Symbol], byte_offset: usize) -> Option<&Symbol> {
986 symbols
987 .iter()
988 .rfind(|s| s.byte_start <= byte_offset && byte_offset <= s.byte_end)
989}
990
991fn call_syntax_kind(name_node: tree_sitter::Node, call_node: tree_sitter::Node) -> CallSyntaxKind {
992 let Some(mut ancestor) = name_node.parent() else {
993 return CallSyntaxKind::Other;
994 };
995 if ancestor.id() == call_node.id() {
996 return CallSyntaxKind::Bare;
997 }
998
999 loop {
1000 if is_memberish_kind(ancestor.kind()) {
1001 return CallSyntaxKind::Member;
1002 }
1003 if ancestor.id() == call_node.id() {
1004 return CallSyntaxKind::Other;
1005 }
1006 let Some(parent) = ancestor.parent() else {
1007 return CallSyntaxKind::Other;
1008 };
1009 ancestor = parent;
1010 }
1011}
1012
1013fn is_memberish_kind(kind: &str) -> bool {
1014 matches!(
1015 kind,
1016 "attribute"
1017 | "member_expression"
1018 | "selector_expression"
1019 | "field_expression"
1020 | "member_access_expression"
1021 | "member_call_expression"
1022 | "navigation_expression"
1023 | "scoped_call_expression"
1024 | "dot"
1025 )
1026}
1027
1028fn is_callable_kind(kind: &str) -> bool {
1029 matches!(kind, "function" | "method")
1030}
1031
1032fn resolve_same_file_callee(
1033 symbols: &[Symbol],
1034 caller_symbol: Option<&Symbol>,
1035 callee_name: &str,
1036 syntax: CallSyntaxKind,
1037) -> Option<String> {
1038 match syntax {
1039 CallSyntaxKind::Bare => unique_symbol_id(
1040 symbols
1041 .iter()
1042 .filter(|symbol| symbol.name == callee_name && is_callable_kind(&symbol.kind)),
1043 ),
1044 CallSyntaxKind::Member => {
1045 let parent_symbol_id =
1046 caller_symbol.and_then(|symbol| symbol.parent_symbol_id.as_deref())?;
1047 unique_symbol_id(symbols.iter().filter(|symbol| {
1048 symbol.name == callee_name
1049 && symbol.kind == "method"
1050 && symbol.parent_symbol_id.as_deref() == Some(parent_symbol_id)
1051 }))
1052 }
1053 CallSyntaxKind::Other => None,
1054 }
1055}
1056
1057fn resolve_same_file_callee_for_language(
1058 language: &str,
1059 symbols: &[Symbol],
1060 caller_symbol: Option<&Symbol>,
1061 callee_name: &str,
1062 syntax: CallSyntaxKind,
1063) -> Option<String> {
1064 if language == "ruby" && syntax == CallSyntaxKind::Bare {
1065 return None;
1068 }
1069 resolve_same_file_callee(symbols, caller_symbol, callee_name, syntax)
1070}
1071
1072fn unique_symbol_id<'a>(symbols: impl Iterator<Item = &'a Symbol>) -> Option<String> {
1073 let mut symbols = symbols;
1074 let first = symbols.next()?;
1075 if symbols.next().is_some() {
1076 None
1077 } else {
1078 Some(first.id.clone())
1079 }
1080}
1081
1082fn member_qualifier_path(
1083 source: &[u8],
1084 call_node: tree_sitter::Node,
1085 name_node: tree_sitter::Node,
1086) -> Option<String> {
1087 let prefix = String::from_utf8_lossy(&source[call_node.start_byte()..name_node.start_byte()]);
1088 let prefix = prefix.trim();
1089 if prefix.starts_with('$') || prefix.contains("->") || prefix.contains("?->") {
1090 return None;
1091 }
1092 let is_absolute_namespace = prefix.starts_with('\\');
1093
1094 let mut chars = prefix
1095 .trim_end_matches(|ch: char| ch == '.' || ch == ':' || ch == '\\' || ch.is_whitespace())
1096 .chars()
1097 .skip_while(|c| !is_identifier_start(*c));
1098 let first = chars.next()?;
1099 if !is_identifier_start(first) {
1100 return None;
1101 }
1102
1103 let mut qualifier = if is_absolute_namespace {
1104 "\\".to_string()
1105 } else {
1106 String::new()
1107 };
1108 qualifier.push(first);
1109 for ch in chars {
1110 if is_identifier_continue(ch) || matches!(ch, '.' | ':' | '\\') {
1111 qualifier.push(ch);
1112 } else {
1113 break;
1114 }
1115 }
1116
1117 let qualifier = qualifier.trim_end_matches(['.', ':', '\\']).to_string();
1118 if qualifier.is_empty() {
1119 None
1120 } else {
1121 Some(qualifier)
1122 }
1123}
1124
1125fn call_qualifier_path(
1126 qualifier_from_name: Option<String>,
1127 qualifier_from_member: impl FnOnce() -> Option<String>,
1128) -> Option<String> {
1129 qualifier_from_name.or_else(qualifier_from_member)
1130}
1131
1132fn external_call_is_shadowed(
1133 source: &[u8],
1134 caller_symbol: Option<&Symbol>,
1135 call_byte: usize,
1136 callee_name: &str,
1137 root_alias: Option<&str>,
1138 syntax: CallSyntaxKind,
1139) -> bool {
1140 let shadow_name = match syntax {
1141 CallSyntaxKind::Bare => Some(callee_name),
1142 CallSyntaxKind::Member => root_alias,
1143 CallSyntaxKind::Other => None,
1144 };
1145 let Some(shadow_name) = shadow_name.filter(|name| !name.is_empty()) else {
1146 return false;
1147 };
1148 local_name_in_scope_before_call(source, caller_symbol, call_byte, shadow_name)
1149}
1150
1151fn local_name_in_scope_before_call(
1152 source: &[u8],
1153 caller_symbol: Option<&Symbol>,
1154 call_byte: usize,
1155 name: &str,
1156) -> bool {
1157 let start = caller_symbol.map(|symbol| symbol.byte_start).unwrap_or(0);
1158 if start >= source.len() || start >= call_byte {
1159 return false;
1160 }
1161 let end = call_byte.min(source.len());
1162 let prefix = String::from_utf8_lossy(&source[start..end]);
1163 caller_symbol.is_some_and(|_| parameter_list_contains_name(&prefix, name))
1164 || prefix
1165 .lines()
1166 .any(|line| local_binding_line_defines(line, name))
1167}
1168
1169fn parameter_list_contains_name(prefix: &str, name: &str) -> bool {
1170 let Some(open) = prefix.find('(') else {
1171 return false;
1172 };
1173 let Some(close) = matching_paren_in_str(prefix, open) else {
1174 return false;
1175 };
1176 prefix[open + 1..close]
1177 .split(',')
1178 .any(|param| parameter_segment_name(param).is_some_and(|param_name| param_name == name))
1179}
1180
1181fn matching_paren_in_str(text: &str, open: usize) -> Option<usize> {
1182 let mut depth = 0usize;
1183 for (idx, ch) in text.char_indices().skip_while(|(idx, _)| *idx < open) {
1184 match ch {
1185 '(' => depth += 1,
1186 ')' => {
1187 depth = depth.saturating_sub(1);
1188 if depth == 0 {
1189 return Some(idx);
1190 }
1191 }
1192 _ => {}
1193 }
1194 }
1195 None
1196}
1197
1198fn parameter_segment_name(segment: &str) -> Option<&str> {
1199 let segment = segment
1200 .split('=')
1201 .next()
1202 .unwrap_or(segment)
1203 .split(':')
1204 .next()
1205 .unwrap_or(segment)
1206 .trim();
1207 segment
1208 .split_whitespace()
1209 .find(|token| token.chars().next().is_some_and(is_identifier_start))
1210 .map(trim_identifier_token)
1211 .filter(|token| !token.is_empty())
1212}
1213
1214fn local_binding_line_defines(line: &str, name: &str) -> bool {
1215 let line = line.trim_start();
1216 if line.is_empty()
1217 || line.starts_with("//")
1218 || line.starts_with('#')
1219 || line.starts_with("import ")
1220 || line.starts_with("from ")
1221 || line.starts_with("use ")
1222 {
1223 return false;
1224 }
1225 if let Some(left) = line.split(":=").next()
1226 && line.contains(":=")
1227 && binding_left_side_contains(left, name)
1228 {
1229 return true;
1230 }
1231 if let Some((left, _)) = split_assignment(line)
1232 && binding_left_side_contains(left, name)
1233 {
1234 return true;
1235 }
1236 declaration_without_assignment_contains(line, name)
1237}
1238
1239fn split_assignment(line: &str) -> Option<(&str, &str)> {
1240 for (idx, ch) in line.char_indices() {
1241 if ch != '=' {
1242 continue;
1243 }
1244 let previous = line[..idx].chars().next_back();
1245 let next = line[idx + 1..].chars().next();
1246 if matches!(
1247 previous,
1248 Some('=' | '!' | '<' | '>' | ':' | '+' | '-' | '*' | '/' | '%')
1249 ) || matches!(next, Some('=' | '>'))
1250 {
1251 continue;
1252 }
1253 return Some((&line[..idx], &line[idx + 1..]));
1254 }
1255 None
1256}
1257
1258fn binding_left_side_contains(left: &str, name: &str) -> bool {
1259 left.split(',')
1260 .filter_map(|part| binding_name_from_left_part(part))
1261 .any(|binding_name| binding_name == name)
1262}
1263
1264fn binding_name_from_left_part(part: &str) -> Option<&str> {
1265 let part = part.trim();
1266 if part.contains(['.', '[', ']']) {
1267 return None;
1268 }
1269 part.split_whitespace()
1270 .next_back()
1271 .map(trim_identifier_token)
1272 .filter(|token| !token.is_empty())
1273}
1274
1275fn declaration_without_assignment_contains(line: &str, name: &str) -> bool {
1276 let Some(rest) = line
1277 .strip_prefix("let ")
1278 .or_else(|| line.strip_prefix("const "))
1279 .or_else(|| line.strip_prefix("var "))
1280 .or_else(|| line.strip_prefix("final "))
1281 .or_else(|| line.strip_prefix("late "))
1282 .or_else(|| line.strip_prefix("val "))
1283 .or_else(|| line.strip_prefix("auto "))
1284 else {
1285 return false;
1286 };
1287 rest.split([',', ';'])
1288 .filter_map(binding_name_from_left_part)
1289 .any(|binding_name| binding_name == name)
1290}
1291
1292fn trim_identifier_token(token: &str) -> &str {
1293 token.trim_matches(|ch: char| !is_identifier_continue(ch))
1294}
1295
1296fn split_qualified_callee(raw: &str) -> (String, Option<String>) {
1297 let raw = raw.trim();
1298 for separator in ["::", "\\", "."] {
1299 if let Some((qualifier, name)) = raw.rsplit_once(separator)
1300 && !qualifier.is_empty()
1301 && !name.is_empty()
1302 {
1303 return (name.to_string(), Some(qualifier.to_string()));
1304 }
1305 }
1306 (raw.to_string(), None)
1307}
1308
1309fn qualifier_root_alias(qualifier: &str) -> Option<&str> {
1310 qualifier
1311 .trim_start_matches('\\')
1312 .split(['.', ':', '\\'])
1313 .find(|part| !part.is_empty())
1314}
1315
1316fn is_identifier_start(ch: char) -> bool {
1317 ch.is_ascii_alphabetic() || matches!(ch, '_' | '$')
1318}
1319
1320fn is_identifier_continue(ch: char) -> bool {
1321 ch.is_ascii_alphanumeric() || matches!(ch, '_' | '$')
1322}
1323
1324fn is_textual_call_name_byte(byte: u8) -> bool {
1325 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'$' | b'!' | b'?')
1326}
1327
1328fn should_ignore_call_name(language: &str, name: &str) -> bool {
1329 match language {
1330 "dart" => matches!(
1331 name,
1332 "if" | "for" | "while" | "switch" | "catch" | "assert" | "return" | "throw"
1333 ),
1334 "elixir" => matches!(
1335 name,
1336 "def" | "defp" | "defmacro" | "defmodule" | "alias" | "import" | "use" | "require"
1337 ),
1338 "kotlin" => matches!(
1339 name,
1340 "if" | "for" | "while" | "when" | "catch" | "return" | "throw"
1341 ),
1342 _ => false,
1343 }
1344}
1345
1346#[cfg(test)]
1347mod tests {
1348 use std::fs;
1349 use std::path::{Path, PathBuf};
1350
1351 use tempfile::TempDir;
1352
1353 use super::*;
1354
1355 fn parse_source(file_name: &str, source: &str, extra_files: &[(&str, &str)]) -> ParseResult {
1356 let tempdir = TempDir::new().expect("create tempdir");
1357 let root = tempdir.path();
1358 for (path, contents) in extra_files {
1359 let file_path = root.join(path);
1360 if let Some(parent) = file_path.parent() {
1361 fs::create_dir_all(parent).expect("create parent dirs");
1362 }
1363 fs::write(&file_path, contents).expect("write extra source");
1364 }
1365
1366 let path = root.join(file_name);
1367 if let Some(parent) = path.parent() {
1368 fs::create_dir_all(parent).expect("create parent dirs");
1369 }
1370 fs::write(&path, source).expect("write test source");
1371 let candidates = discover_supported_files(root);
1372 let context = build_import_resolution_context(root, &candidates);
1373 parse_file_with_semantic(&path, "proj", root, &[], &context, None)
1374 .expect("parse result")
1375 .expect("parse file")
1376 }
1377
1378 fn parse_python(source: &str) -> ParseResult {
1379 parse_source("sample.py", source, &[])
1380 }
1381
1382 fn parse_javascript(source: &str, extra_files: &[(&str, &str)]) -> ParseResult {
1383 parse_source("src/sample.js", source, extra_files)
1384 }
1385
1386 fn parse_typescript(source: &str, extra_files: &[(&str, &str)]) -> ParseResult {
1387 parse_source("src/sample.ts", source, extra_files)
1388 }
1389
1390 fn parse_go(source: &str, extra_files: &[(&str, &str)]) -> ParseResult {
1391 parse_source("cmd/sample.go", source, extra_files)
1392 }
1393
1394 fn parse_rust(source: &str, extra_files: &[(&str, &str)]) -> ParseResult {
1395 parse_source("src/main.rs", source, extra_files)
1396 }
1397
1398 fn parse_java(source: &str, extra_files: &[(&str, &str)]) -> ParseResult {
1399 parse_source("src/main/java/app/Sample.java", source, extra_files)
1400 }
1401
1402 fn parse_csharp(source: &str, extra_files: &[(&str, &str)]) -> ParseResult {
1403 parse_source("src/Sample.cs", source, extra_files)
1404 }
1405
1406 fn parse_php(source: &str, extra_files: &[(&str, &str)]) -> ParseResult {
1407 parse_source("src/sample.php", source, extra_files)
1408 }
1409
1410 fn parse_ruby(source: &str, extra_files: &[(&str, &str)]) -> ParseResult {
1411 parse_source("lib/sample.rb", source, extra_files)
1412 }
1413
1414 fn parse_dart(source: &str, extra_files: &[(&str, &str)]) -> ParseResult {
1415 parse_source("lib/sample.dart", source, extra_files)
1416 }
1417
1418 fn parse_elixir(source: &str, extra_files: &[(&str, &str)]) -> ParseResult {
1419 parse_source("lib/sample.ex", source, extra_files)
1420 }
1421
1422 fn parse_kotlin(source: &str, extra_files: &[(&str, &str)]) -> ParseResult {
1423 parse_source("src/main/kotlin/Sample.kt", source, extra_files)
1424 }
1425
1426 fn parse_swift(source: &str, extra_files: &[(&str, &str)]) -> ParseResult {
1427 parse_source("Sources/App/main.swift", source, extra_files)
1428 }
1429
1430 fn discover_supported_files(root: &Path) -> Vec<PathBuf> {
1431 let mut candidates = Vec::new();
1432 let mut stack = vec![root.to_path_buf()];
1433 while let Some(path) = stack.pop() {
1434 let entries = fs::read_dir(&path).expect("read dir");
1435 for entry in entries {
1436 let entry = entry.expect("dir entry");
1437 let entry_path = entry.path();
1438 if entry_path.is_dir() {
1439 stack.push(entry_path);
1440 } else if let Some(language) =
1441 languages::detect_language(&entry_path.to_string_lossy())
1442 && !language.is_empty()
1443 {
1444 candidates.push(entry_path);
1445 }
1446 }
1447 }
1448 candidates
1449 }
1450
1451 #[test]
1452 fn line_terminator_len_tracks_lf_crlf_and_eof() {
1453 let text = "import 'a';\r\nhttp.Client();\nlast()";
1454 assert_eq!(line_terminator_len(text, 0, "import 'a';".len()), 2);
1455
1456 let second_start = "import 'a';\r\n".len();
1457 assert_eq!(
1458 line_terminator_len(text, second_start, "http.Client();".len()),
1459 1
1460 );
1461
1462 let last_start = "import 'a';\r\nhttp.Client();\n".len();
1463 assert_eq!(line_terminator_len(text, last_start, "last()".len()), 0);
1464 }
1465
1466 struct FakeSemanticResolver {
1467 target: Option<crate::index::semantic::SemanticCallTarget>,
1468 expected_language: &'static str,
1469 expected_callee: &'static str,
1470 requests: Vec<CapturedSemanticRequest>,
1471 error: Option<&'static str>,
1472 }
1473
1474 struct CapturedSemanticRequest {
1475 language: String,
1476 file_path: PathBuf,
1477 root_path: PathBuf,
1478 callee_name: String,
1479 line: usize,
1480 column: usize,
1481 }
1482
1483 impl SemanticCallResolver for FakeSemanticResolver {
1484 fn resolve(
1485 &mut self,
1486 request: &SemanticCallRequest<'_>,
1487 ) -> anyhow::Result<Option<crate::index::semantic::SemanticCallTarget>> {
1488 self.requests.push(CapturedSemanticRequest {
1489 language: request.language.to_string(),
1490 file_path: request.file_path.to_path_buf(),
1491 root_path: request.root_path.to_path_buf(),
1492 callee_name: request.callee_name.to_string(),
1493 line: request.line,
1494 column: request.column,
1495 });
1496 if let Some(error) = self.error {
1497 anyhow::bail!("{error}");
1498 }
1499 if request.language == self.expected_language
1500 && request.callee_name == self.expected_callee
1501 {
1502 Ok(self.target.clone())
1503 } else {
1504 Ok(None)
1505 }
1506 }
1507 }
1508
1509 #[test]
1510 fn explicit_qualified_raw_callee_takes_precedence_over_member_prefix() {
1511 let mut inferred_called = false;
1512 let (_, qualifier_from_name) = split_qualified_callee("Vendor\\Pkg\\helper");
1513
1514 let qualifier_path = call_qualifier_path(qualifier_from_name, || {
1515 inferred_called = true;
1516 Some("Vendor".to_string())
1517 });
1518
1519 assert_eq!(qualifier_path.as_deref(), Some("Vendor\\Pkg"));
1520 assert!(!inferred_called);
1521 }
1522
1523 #[test]
1524 fn resolves_unique_same_file_bare_calls() {
1525 let parsed = parse_python(
1526 r#"
1527def foo():
1528 pass
1529
1530def bar():
1531 foo()
1532"#,
1533 );
1534
1535 let foo = parsed
1536 .symbols
1537 .iter()
1538 .find(|symbol| symbol.name == "foo")
1539 .expect("foo symbol");
1540 let bar = parsed
1541 .symbols
1542 .iter()
1543 .find(|symbol| symbol.name == "bar")
1544 .expect("bar symbol");
1545 let call = parsed.calls.first().expect("call");
1546
1547 assert_eq!(call.caller_symbol_id, bar.id);
1548 assert_eq!(call.callee_symbol_id.as_deref(), Some(foo.id.as_str()));
1549 }
1550
1551 #[test]
1552 fn resolves_same_class_member_calls() {
1553 let parsed = parse_python(
1554 r#"
1555class Greeter:
1556 def greet(self):
1557 self.render()
1558
1559 def render(self):
1560 pass
1561"#,
1562 );
1563
1564 let render = parsed
1565 .symbols
1566 .iter()
1567 .find(|symbol| symbol.qualified_name == "Greeter.render")
1568 .expect("render method");
1569 let call = parsed.calls.first().expect("call");
1570
1571 assert_eq!(call.callee_symbol_id.as_deref(), Some(render.id.as_str()));
1572 }
1573
1574 #[test]
1575 fn leaves_ambiguous_bare_calls_unresolved() {
1576 let parsed = parse_python(
1577 r#"
1578def foo():
1579 pass
1580
1581class A:
1582 def foo(self):
1583 pass
1584
1585def bar():
1586 foo()
1587"#,
1588 );
1589
1590 let call = parsed.calls.first().expect("call");
1591 assert!(call.callee_symbol_id.is_none());
1592 }
1593
1594 #[test]
1595 fn leaves_non_local_member_calls_unresolved() {
1596 let parsed = parse_python(
1597 r#"
1598class A:
1599 def bar(self):
1600 obj.render()
1601
1602class B:
1603 def render(self):
1604 pass
1605"#,
1606 );
1607
1608 let call = parsed.calls.first().expect("call");
1609 assert!(call.callee_symbol_id.is_none());
1610 }
1611
1612 #[test]
1613 fn classifies_external_python_from_import_calls() {
1614 let parsed = parse_python(
1615 r#"
1616from requests import get as fetch
1617
1618def run():
1619 fetch()
1620"#,
1621 );
1622
1623 let call = parsed.calls.first().expect("call");
1624 assert_eq!(call.callee_target_kind.as_str(), "external");
1625 assert_eq!(call.callee_name, "get");
1626 assert_eq!(call.callee_external_module.as_deref(), Some("requests"));
1627 }
1628
1629 #[test]
1630 fn leaves_local_python_imports_unresolved() {
1631 let parsed = parse_source(
1632 "pkg/main.py",
1633 r#"
1634from pkg.utils import helper
1635
1636def run():
1637 helper()
1638"#,
1639 &[("pkg/utils.py", "def helper():\n pass\n")],
1640 );
1641
1642 let call = parsed.calls.first().expect("call");
1643 assert_eq!(call.callee_target_kind.as_str(), "unresolved");
1644 assert!(call.callee_external_module.is_none());
1645 }
1646
1647 #[test]
1648 fn classifies_external_javascript_named_import_calls() {
1649 let parsed = parse_javascript(
1650 r#"
1651import { useState } from "react";
1652
1653function run() {
1654 useState();
1655}
1656"#,
1657 &[(
1658 "package.json",
1659 r#"{"name":"app","dependencies":{"react":"^18.0.0"}}"#,
1660 )],
1661 );
1662
1663 let call = parsed.calls.first().expect("call");
1664 assert_eq!(call.callee_target_kind.as_str(), "external");
1665 assert_eq!(call.callee_name, "useState");
1666 assert_eq!(call.callee_external_module.as_deref(), Some("react"));
1667 }
1668
1669 #[test]
1670 fn leaves_external_bare_calls_shadowed_by_parameters_unresolved() {
1671 let parsed = parse_javascript(
1672 r#"
1673import { useState } from "react";
1674
1675function run(useState) {
1676 useState();
1677}
1678"#,
1679 &[(
1680 "package.json",
1681 r#"{"name":"app","dependencies":{"react":"^18.0.0"}}"#,
1682 )],
1683 );
1684
1685 let call = parsed.calls.first().expect("call");
1686 assert_eq!(call.callee_target_kind.as_str(), "unresolved");
1687 assert!(call.callee_external_module.is_none());
1688 }
1689
1690 #[test]
1691 fn classifies_external_javascript_namespace_member_calls() {
1692 let parsed = parse_javascript(
1693 r#"
1694import * as React from "react";
1695
1696function run() {
1697 React.useState();
1698}
1699"#,
1700 &[(
1701 "package.json",
1702 r#"{"name":"app","dependencies":{"react":"^18.0.0"}}"#,
1703 )],
1704 );
1705
1706 let call = parsed.calls.first().expect("call");
1707 assert_eq!(call.callee_target_kind.as_str(), "external");
1708 assert_eq!(call.callee_name, "useState");
1709 assert_eq!(call.callee_external_module.as_deref(), Some("react"));
1710 }
1711
1712 #[test]
1713 fn leaves_relative_javascript_imports_unresolved() {
1714 let parsed = parse_javascript(
1715 r#"
1716import { helper } from "./utils";
1717
1718function run() {
1719 helper();
1720}
1721"#,
1722 &[
1723 (
1724 "package.json",
1725 r#"{"name":"app","dependencies":{"react":"^18.0.0"}}"#,
1726 ),
1727 ("src/utils.js", "export function helper() {}\n"),
1728 ],
1729 );
1730
1731 let call = parsed.calls.first().expect("call");
1732 assert_eq!(call.callee_target_kind.as_str(), "unresolved");
1733 }
1734
1735 #[test]
1736 fn classifies_external_typescript_default_member_calls() {
1737 let parsed = parse_typescript(
1738 r#"
1739import React from "react";
1740
1741function run() {
1742 React.useState();
1743}
1744"#,
1745 &[(
1746 "package.json",
1747 r#"{"name":"app","dependencies":{"react":"^18.0.0"}}"#,
1748 )],
1749 );
1750
1751 let call = parsed.calls.first().expect("call");
1752 assert_eq!(call.callee_target_kind.as_str(), "external");
1753 assert_eq!(call.callee_name, "useState");
1754 assert_eq!(call.callee_external_module.as_deref(), Some("react"));
1755 }
1756
1757 #[test]
1758 fn leaves_external_qualified_roots_shadowed_by_locals_unresolved() {
1759 let parsed = parse_typescript(
1760 r#"
1761import React from "react";
1762
1763function run() {
1764 const React = makeLocalReact();
1765 React.useState();
1766}
1767"#,
1768 &[(
1769 "package.json",
1770 r#"{"name":"app","dependencies":{"react":"^18.0.0"}}"#,
1771 )],
1772 );
1773
1774 let call = parsed
1775 .calls
1776 .iter()
1777 .find(|call| call.callee_name == "useState")
1778 .expect("call");
1779 assert_eq!(call.callee_target_kind.as_str(), "unresolved");
1780 assert!(call.callee_external_module.is_none());
1781 }
1782
1783 #[test]
1784 fn leaves_unlisted_javascript_package_aliases_unresolved() {
1785 let parsed = parse_javascript(
1786 r#"
1787import { helper } from "@/utils";
1788
1789function run() {
1790 helper();
1791}
1792"#,
1793 &[(
1794 "package.json",
1795 r#"{"name":"app","dependencies":{"react":"^18.0.0"}}"#,
1796 )],
1797 );
1798
1799 let call = parsed.calls.first().expect("call");
1800 assert_eq!(call.callee_target_kind.as_str(), "unresolved");
1801 }
1802
1803 #[test]
1804 fn classifies_external_go_import_alias_selector_calls() {
1805 let parsed = parse_go(
1806 r#"
1807package main
1808
1809import (
1810 "fmt"
1811 cli "github.com/acme/client"
1812 "gopkg.in/yaml.v3"
1813)
1814
1815func run() {
1816 fmt.Println("hello")
1817 cli.Connect()
1818 yaml.Unmarshal(nil, nil)
1819}
1820"#,
1821 &[("go.mod", "module example.com/app\n")],
1822 );
1823
1824 let fmt_call = parsed
1825 .calls
1826 .iter()
1827 .find(|call| call.callee_name == "Println")
1828 .expect("fmt call");
1829 assert_eq!(fmt_call.callee_target_kind.as_str(), "external");
1830 assert_eq!(fmt_call.callee_external_module.as_deref(), Some("fmt"));
1831
1832 let alias_call = parsed
1833 .calls
1834 .iter()
1835 .find(|call| call.callee_name == "Connect")
1836 .expect("alias call");
1837 assert_eq!(alias_call.callee_target_kind.as_str(), "external");
1838 assert_eq!(
1839 alias_call.callee_external_module.as_deref(),
1840 Some("github.com/acme/client")
1841 );
1842
1843 let yaml_call = parsed
1844 .calls
1845 .iter()
1846 .find(|call| call.callee_name == "Unmarshal")
1847 .expect("yaml call");
1848 assert_eq!(yaml_call.callee_target_kind.as_str(), "external");
1849 assert_eq!(
1850 yaml_call.callee_external_module.as_deref(),
1851 Some("gopkg.in/yaml.v3")
1852 );
1853 }
1854
1855 #[test]
1856 fn leaves_self_module_go_imports_unresolved() {
1857 let parsed = parse_go(
1858 r#"
1859package main
1860
1861import "example.com/app/pkg/tool"
1862
1863func run() {
1864 tool.Run()
1865}
1866"#,
1867 &[("go.mod", "module example.com/app\n")],
1868 );
1869
1870 let call = parsed.calls.first().expect("call");
1871 assert_eq!(call.callee_target_kind.as_str(), "unresolved");
1872 }
1873
1874 #[test]
1875 fn leaves_unimported_go_selector_calls_unresolved() {
1876 let parsed = parse_go(
1877 r#"
1878package main
1879
1880func run(client Client) {
1881 client.Do()
1882}
1883"#,
1884 &[("go.mod", "module example.com/app\n")],
1885 );
1886
1887 let call = parsed.calls.first().expect("call");
1888 assert_eq!(call.callee_target_kind.as_str(), "unresolved");
1889 }
1890
1891 #[test]
1892 fn classifies_external_rust_use_alias_and_path_calls() {
1893 let parsed = parse_rust(
1894 r#"
1895use serde_json::from_str as parse_json;
1896use std::fs;
1897
1898fn run() {
1899 parse_json("{}");
1900 serde_json::from_str("{}");
1901 fs::read("Cargo.toml");
1902 std::fs::read("Cargo.toml");
1903}
1904"#,
1905 &[(
1906 "Cargo.toml",
1907 r#"[package]
1908name = "app"
1909
1910[dependencies]
1911serde_json = { version = "1" }
1912"#,
1913 )],
1914 );
1915
1916 let parse_call = parsed
1917 .calls
1918 .iter()
1919 .find(|call| call.callee_name == "from_str")
1920 .expect("from_str call");
1921 assert_eq!(parse_call.callee_target_kind.as_str(), "external");
1922 assert_eq!(
1923 parse_call.callee_external_module.as_deref(),
1924 Some("serde_json")
1925 );
1926
1927 let read_modules: Vec<_> = parsed
1928 .calls
1929 .iter()
1930 .filter(|call| call.callee_name == "read")
1931 .map(|call| call.callee_external_module.as_deref())
1932 .collect();
1933 assert_eq!(read_modules, vec![Some("std::fs"), Some("std::fs")]);
1934 }
1935
1936 #[test]
1937 fn leaves_rust_self_crate_and_glob_imports_unresolved() {
1938 let parsed = parse_rust(
1939 r#"
1940use app::helper;
1941use serde_json::*;
1942
1943fn run() {
1944 helper();
1945 from_str("{}");
1946}
1947"#,
1948 &[(
1949 "Cargo.toml",
1950 r#"[package]
1951name = "app"
1952
1953[dependencies]
1954serde_json = "1"
1955"#,
1956 )],
1957 );
1958
1959 assert_eq!(parsed.calls.len(), 2);
1960 assert!(
1961 parsed
1962 .calls
1963 .iter()
1964 .all(|call| call.callee_target_kind.as_str() == "unresolved")
1965 );
1966 }
1967
1968 #[test]
1969 fn leaves_rust_receiver_method_calls_unresolved() {
1970 let parsed = parse_rust(
1971 r#"
1972fn run(value: Parser) {
1973 value.parse();
1974}
1975"#,
1976 &[(
1977 "Cargo.toml",
1978 r#"[package]
1979name = "app"
1980"#,
1981 )],
1982 );
1983
1984 let call = parsed.calls.first().expect("call");
1985 assert_eq!(call.callee_target_kind.as_str(), "unresolved");
1986 }
1987
1988 #[test]
1989 fn classifies_external_java_import_and_static_import_calls() {
1990 let parsed = parse_java(
1991 r#"
1992package app;
1993
1994import java.util.Collections;
1995import static java.util.Objects.requireNonNull;
1996
1997class Sample {
1998 void run() {
1999 Collections.emptyList();
2000 requireNonNull("x");
2001 }
2002}
2003"#,
2004 &[],
2005 );
2006
2007 let class_call = parsed
2008 .calls
2009 .iter()
2010 .find(|call| call.callee_name == "emptyList")
2011 .expect("class call");
2012 assert_eq!(class_call.callee_target_kind.as_str(), "external");
2013 assert_eq!(
2014 class_call.callee_external_module.as_deref(),
2015 Some("java.util.Collections")
2016 );
2017
2018 let static_call = parsed
2019 .calls
2020 .iter()
2021 .find(|call| call.callee_name == "requireNonNull")
2022 .expect("static call");
2023 assert_eq!(static_call.callee_target_kind.as_str(), "external");
2024 assert_eq!(
2025 static_call.callee_external_module.as_deref(),
2026 Some("java.util.Objects")
2027 );
2028 }
2029
2030 #[test]
2031 fn leaves_java_wildcard_and_instance_calls_unresolved() {
2032 let parsed = parse_java(
2033 r#"
2034package app;
2035
2036import java.util.*;
2037
2038class Sample {
2039 void run(java.util.List<String> list) {
2040 list.add("x");
2041 emptyList();
2042 }
2043}
2044"#,
2045 &[],
2046 );
2047
2048 assert_eq!(parsed.calls.len(), 2);
2049 assert!(
2050 parsed
2051 .calls
2052 .iter()
2053 .all(|call| call.callee_target_kind.as_str() == "unresolved")
2054 );
2055 }
2056
2057 #[test]
2058 fn leaves_local_java_imports_unresolved() {
2059 let parsed = parse_java(
2060 r#"
2061package app;
2062
2063import app.helpers.Helper;
2064
2065class Sample {
2066 void run() {
2067 Helper.render();
2068 }
2069}
2070"#,
2071 &[(
2072 "src/main/java/app/helpers/Helper.java",
2073 r#"
2074package app.helpers;
2075
2076class Helper {
2077 static void render() {}
2078}
2079"#,
2080 )],
2081 );
2082
2083 let call = parsed.calls.first().expect("call");
2084 assert_eq!(call.callee_target_kind.as_str(), "unresolved");
2085 }
2086
2087 #[test]
2088 fn classifies_external_csharp_alias_static_and_qualified_calls() {
2089 let parsed = parse_csharp(
2090 r#"
2091using Json = Newtonsoft.Json.JsonConvert;
2092using static System.Math;
2093using System;
2094
2095class Sample {
2096 void Run() {
2097 Json.SerializeObject(this);
2098 Sqrt(4);
2099 System.Console.WriteLine("x");
2100 }
2101}
2102"#,
2103 &[],
2104 );
2105
2106 let alias_call = parsed
2107 .calls
2108 .iter()
2109 .find(|call| call.callee_name == "SerializeObject")
2110 .expect("alias call");
2111 assert_eq!(alias_call.callee_target_kind.as_str(), "external");
2112 assert_eq!(
2113 alias_call.callee_external_module.as_deref(),
2114 Some("Newtonsoft.Json.JsonConvert")
2115 );
2116
2117 let static_call = parsed
2118 .calls
2119 .iter()
2120 .find(|call| call.callee_name == "Sqrt")
2121 .expect("static call");
2122 assert_eq!(static_call.callee_target_kind.as_str(), "external");
2123 assert_eq!(
2124 static_call.callee_external_module.as_deref(),
2125 Some("System.Math")
2126 );
2127
2128 let qualified_call = parsed
2129 .calls
2130 .iter()
2131 .find(|call| call.callee_name == "WriteLine")
2132 .expect("qualified call");
2133 assert_eq!(qualified_call.callee_target_kind.as_str(), "external");
2134 assert_eq!(
2135 qualified_call.callee_external_module.as_deref(),
2136 Some("System.Console")
2137 );
2138 }
2139
2140 #[test]
2141 fn leaves_csharp_instance_and_local_namespace_calls_unresolved() {
2142 let parsed = parse_csharp(
2143 r#"
2144namespace App;
2145
2146using App.Helpers;
2147
2148class Sample {
2149 void Run(Client client) {
2150 client.Send();
2151 App.Helpers.Tool.Render();
2152 }
2153}
2154"#,
2155 &[],
2156 );
2157
2158 assert_eq!(parsed.calls.len(), 2);
2159 assert!(
2160 parsed
2161 .calls
2162 .iter()
2163 .all(|call| call.callee_target_kind.as_str() == "unresolved")
2164 );
2165 }
2166
2167 #[test]
2168 fn classifies_external_php_namespace_and_fully_qualified_calls() {
2169 let parsed = parse_php(
2170 r#"
2171<?php
2172namespace App;
2173
2174use Vendor\Pkg\Client as ApiClient;
2175use function Vendor\Pkg\do_work as work;
2176
2177function run() {
2178 ApiClient::connect();
2179 work();
2180 \Vendor\Pkg\helper();
2181 \Vendor\Pkg\Service::build();
2182}
2183"#,
2184 &[],
2185 );
2186
2187 let static_call = parsed
2188 .calls
2189 .iter()
2190 .find(|call| call.callee_name == "connect")
2191 .expect("static call");
2192 assert_eq!(static_call.callee_target_kind.as_str(), "external");
2193 assert_eq!(
2194 static_call.callee_external_module.as_deref(),
2195 Some("Vendor\\Pkg\\Client")
2196 );
2197
2198 let function_import_call = parsed
2199 .calls
2200 .iter()
2201 .find(|call| call.callee_name == "do_work")
2202 .expect("function import call");
2203 assert_eq!(function_import_call.callee_target_kind.as_str(), "external");
2204 assert_eq!(
2205 function_import_call.callee_external_module.as_deref(),
2206 Some("Vendor\\Pkg")
2207 );
2208
2209 let qualified_function_call = parsed
2210 .calls
2211 .iter()
2212 .find(|call| call.callee_name == "helper")
2213 .expect("qualified function call");
2214 assert_eq!(
2215 qualified_function_call.callee_target_kind.as_str(),
2216 "external"
2217 );
2218 assert_eq!(
2219 qualified_function_call.callee_external_module.as_deref(),
2220 Some("Vendor\\Pkg")
2221 );
2222
2223 let qualified_static_call = parsed
2224 .calls
2225 .iter()
2226 .find(|call| call.callee_name == "build")
2227 .expect("qualified static call");
2228 assert_eq!(
2229 qualified_static_call.callee_target_kind.as_str(),
2230 "external"
2231 );
2232 assert_eq!(
2233 qualified_static_call.callee_external_module.as_deref(),
2234 Some("Vendor\\Pkg\\Service")
2235 );
2236 }
2237
2238 #[test]
2239 fn leaves_php_dynamic_member_and_local_import_calls_unresolved() {
2240 let parsed = parse_php(
2241 r#"
2242<?php
2243namespace App;
2244
2245use App\Local\Client;
2246
2247function run($obj) {
2248 $obj->connect();
2249 Client::connect();
2250 \missing();
2251 missing();
2252}
2253"#,
2254 &[(
2255 "src/Local/Client.php",
2256 r#"
2257<?php
2258namespace App\Local;
2259
2260class Client {}
2261"#,
2262 )],
2263 );
2264
2265 assert!(
2266 parsed
2267 .calls
2268 .iter()
2269 .all(|call| call.callee_target_kind.as_str() == "unresolved")
2270 );
2271 }
2272
2273 #[test]
2274 fn classifies_external_ruby_constant_qualified_require_calls() {
2275 let parsed = parse_ruby(
2276 r#"
2277require "json"
2278require "fileutils"
2279
2280def run
2281 JSON.parse("{}")
2282 FileUtils.mkdir_p("tmp")
2283 parse("{}")
2284end
2285"#,
2286 &[],
2287 );
2288
2289 let json_call = parsed
2290 .calls
2291 .iter()
2292 .find(|call| call.callee_name == "parse")
2293 .expect("json call");
2294 assert_eq!(json_call.callee_target_kind.as_str(), "external");
2295 assert_eq!(json_call.callee_external_module.as_deref(), Some("json"));
2296
2297 let mkdir_call = parsed
2298 .calls
2299 .iter()
2300 .find(|call| call.callee_name == "mkdir_p")
2301 .expect("fileutils call");
2302 assert_eq!(mkdir_call.callee_target_kind.as_str(), "external");
2303 assert_eq!(
2304 mkdir_call.callee_external_module.as_deref(),
2305 Some("fileutils")
2306 );
2307 }
2308
2309 #[test]
2310 fn leaves_ruby_local_constant_collision_and_receivers_unresolved() {
2311 let parsed = parse_ruby(
2312 r#"
2313require "json"
2314
2315def run(client)
2316 JSON.parse("{}")
2317 client.parse("{}")
2318 send(:parse, "{}")
2319end
2320"#,
2321 &[(
2322 "lib/json.rb",
2323 r#"
2324module JSON
2325end
2326"#,
2327 )],
2328 );
2329
2330 assert!(
2331 parsed
2332 .calls
2333 .iter()
2334 .all(|call| call.callee_target_kind.as_str() == "unresolved")
2335 );
2336 }
2337
2338 #[test]
2339 fn classifies_external_dart_alias_calls_only() {
2340 let parsed = parse_dart(
2341 r#"
2342import 'dart:convert' as convert;
2343import 'package:http/http.dart' as http show Client;
2344import 'package:app/local.dart' as local;
2345import './relative.dart' as relative;
2346
2347void run() {
2348 convert.jsonDecode("{}");
2349 http.Client();
2350 local.helper();
2351 relative.helper();
2352 jsonDecode("{}");
2353}
2354"#,
2355 &[(
2356 "pubspec.yaml",
2357 r#"
2358name: app
2359dependencies:
2360 http: ^1.0.0
2361"#,
2362 )],
2363 );
2364
2365 let json_call = parsed
2366 .calls
2367 .iter()
2368 .find(|call| call.callee_name == "jsonDecode")
2369 .expect("jsonDecode call");
2370 assert_eq!(json_call.callee_target_kind.as_str(), "external");
2371 assert_eq!(
2372 json_call.callee_external_module.as_deref(),
2373 Some("dart:convert")
2374 );
2375
2376 let client_call = parsed
2377 .calls
2378 .iter()
2379 .find(|call| call.callee_name == "Client")
2380 .expect("Client call");
2381 assert_eq!(client_call.callee_target_kind.as_str(), "external");
2382 assert_eq!(
2383 client_call.callee_external_module.as_deref(),
2384 Some("package:http/http.dart")
2385 );
2386
2387 let unresolved: Vec<_> = parsed
2388 .calls
2389 .iter()
2390 .filter(|call| matches!(call.callee_name.as_str(), "helper" | "jsonDecode"))
2391 .filter(|call| call.callee_target_kind.as_str() == "unresolved")
2392 .collect();
2393 assert_eq!(unresolved.len(), 3);
2394 assert!(parsed.calls.iter().all(|call| call.callee_name != "run"));
2395 }
2396
2397 #[test]
2398 fn textual_dart_calls_handle_generics_and_ignore_comments_and_strings() {
2399 let parsed = parse_dart(
2400 r#"
2401void run() {
2402 builder<T>();
2403 final text = "fakeCall()";
2404 final other = 'otherCall()';
2405 // commentedCall();
2406 /* blockCall();
2407 stillBlockCall();
2408 */
2409 afterBlock(); // trailingCommentCall();
2410}
2411"#,
2412 &[],
2413 );
2414
2415 let call_names: Vec<_> = parsed
2416 .calls
2417 .iter()
2418 .map(|call| call.callee_name.as_str())
2419 .collect();
2420 assert!(call_names.contains(&"builder"));
2421 assert!(call_names.contains(&"afterBlock"));
2422 for skipped in [
2423 "fakeCall",
2424 "otherCall",
2425 "commentedCall",
2426 "blockCall",
2427 "stillBlockCall",
2428 "trailingCommentCall",
2429 ] {
2430 assert!(!call_names.contains(&skipped), "unexpected call {skipped}");
2431 }
2432 }
2433
2434 #[test]
2435 fn textual_dart_calls_ignore_raw_and_triple_quoted_multiline_strings() {
2436 let parsed = parse_dart(
2437 r#"
2438void run() {
2439 final raw = r"rawCall()";
2440 final triple = '''
2441 tripleCall();
2442 ''';
2443 final rawTriple = r"""
2444 rawTripleCall();
2445 """;
2446 afterStrings();
2447}
2448"#,
2449 &[],
2450 );
2451
2452 let call_names: Vec<_> = parsed
2453 .calls
2454 .iter()
2455 .map(|call| call.callee_name.as_str())
2456 .collect();
2457 assert_eq!(call_names, vec!["afterStrings"]);
2458 }
2459
2460 #[test]
2461 fn classifies_external_elixir_remote_alias_and_required_calls() {
2462 let parsed = parse_elixir(
2463 r#"
2464defmodule App.Sample do
2465 alias HTTPoison, as: HTTP
2466 require Jason
2467
2468 def run(body) do
2469 Jason.decode!(body)
2470 HTTP.get("https://example.com")
2471 end
2472end
2473"#,
2474 &[
2475 (
2476 "mix.exs",
2477 r#"
2478defmodule App.MixProject do
2479 defp deps do
2480 [
2481 {:jason, "~> 1.4"},
2482 {:httpoison, "~> 2.0"}
2483 ]
2484 end
2485end
2486"#,
2487 ),
2488 (
2489 "mix.lock",
2490 r#"{"jason": {:hex, :jason}, "httpoison": {:hex, :httpoison}}"#,
2491 ),
2492 ],
2493 );
2494
2495 let decode_call = parsed
2496 .calls
2497 .iter()
2498 .find(|call| call.callee_name == "decode!")
2499 .expect("decode call");
2500 assert_eq!(decode_call.callee_target_kind.as_str(), "external");
2501 assert_eq!(decode_call.callee_external_module.as_deref(), Some("Jason"));
2502
2503 let get_call = parsed
2504 .calls
2505 .iter()
2506 .find(|call| call.callee_name == "get")
2507 .expect("get call");
2508 assert_eq!(get_call.callee_target_kind.as_str(), "external");
2509 assert_eq!(
2510 get_call.callee_external_module.as_deref(),
2511 Some("HTTPoison")
2512 );
2513 }
2514
2515 #[test]
2516 fn leaves_elixir_local_module_collision_and_imported_calls_unresolved() {
2517 let parsed = parse_elixir(
2518 r#"
2519defmodule App.Sample do
2520 import Jason
2521
2522 def run(body) do
2523 Jason.decode!(body)
2524 decode!(body)
2525 end
2526end
2527"#,
2528 &[
2529 ("mix.exs", "{:jason, \"~> 1.4\"}\n"),
2530 (
2531 "lib/jason.ex",
2532 r#"
2533defmodule Jason do
2534end
2535"#,
2536 ),
2537 ],
2538 );
2539
2540 assert!(
2541 parsed
2542 .calls
2543 .iter()
2544 .all(|call| call.callee_target_kind.as_str() == "unresolved")
2545 );
2546 }
2547
2548 #[test]
2549 fn extracts_kotlin_symbols_imports_and_calls_without_external_classification() {
2550 let parsed = parse_kotlin(
2551 r#"
2552package app
2553
2554import kotlinx.coroutines.runBlocking
2555
2556class Runner {
2557 fun run() {
2558 runBlocking()
2559 println("hello")
2560 }
2561}
2562"#,
2563 &[],
2564 );
2565
2566 assert!(
2567 parsed
2568 .symbols
2569 .iter()
2570 .any(|symbol| symbol.name == "Runner" && symbol.kind == "class")
2571 );
2572 assert!(
2573 parsed
2574 .symbols
2575 .iter()
2576 .any(|symbol| symbol.name == "run" && symbol.kind == "method")
2577 );
2578 assert!(
2579 parsed
2580 .imports
2581 .iter()
2582 .any(|import| import.module_name == "import kotlinx.coroutines.runBlocking")
2583 );
2584 assert!(
2585 parsed
2586 .calls
2587 .iter()
2588 .any(|call| call.callee_name == "runBlocking"
2589 && call.callee_target_kind.as_str() == "unresolved")
2590 );
2591 }
2592
2593 #[test]
2594 fn semantic_resolver_can_classify_cpp_calls_as_external() {
2595 let tempdir = TempDir::new().expect("create tempdir");
2596 let root = tempdir.path();
2597 let path = root.join("src/main.cpp");
2598 fs::create_dir_all(path.parent().expect("parent")).expect("create parent dirs");
2599 fs::write(
2600 &path,
2601 r#"
2602void run() {
2603 printf("x");
2604}
2605"#,
2606 )
2607 .expect("write source");
2608 let candidates = discover_supported_files(root);
2609 let context = build_import_resolution_context(root, &candidates);
2610 let mut resolver = FakeSemanticResolver {
2611 target: Some(crate::index::semantic::SemanticCallTarget {
2612 callee_name: "printf".to_string(),
2613 external_module: "/usr/include/stdio.h".to_string(),
2614 }),
2615 expected_language: "cpp",
2616 expected_callee: "printf",
2617 requests: Vec::new(),
2618 error: None,
2619 };
2620 let parsed =
2621 parse_file_with_semantic(&path, "proj", root, &[], &context, Some(&mut resolver))
2622 .expect("parse result")
2623 .expect("parse file");
2624
2625 let call = parsed.calls.first().expect("printf call");
2626 assert_eq!(call.callee_target_kind.as_str(), "external");
2627 assert_eq!(
2628 call.callee_external_module.as_deref(),
2629 Some("/usr/include/stdio.h")
2630 );
2631 }
2632
2633 #[test]
2634 fn semantic_resolver_can_classify_textual_dart_calls_as_external() {
2635 let tempdir = TempDir::new().expect("create tempdir");
2636 let root = tempdir.path();
2637 let path = root.join("lib/sample.dart");
2638 fs::create_dir_all(path.parent().expect("parent")).expect("create parent dirs");
2639 fs::write(
2640 &path,
2641 r#"
2642void run() {
2643 Tooltip(message: 'x');
2644}
2645"#,
2646 )
2647 .expect("write source");
2648 let candidates = discover_supported_files(root);
2649 let context = build_import_resolution_context(root, &candidates);
2650 let mut resolver = FakeSemanticResolver {
2651 target: Some(crate::index::semantic::SemanticCallTarget {
2652 callee_name: "Tooltip".to_string(),
2653 external_module: "package:flutter/material.dart".to_string(),
2654 }),
2655 expected_language: "dart",
2656 expected_callee: "Tooltip",
2657 requests: Vec::new(),
2658 error: None,
2659 };
2660 let parsed =
2661 parse_file_with_semantic(&path, "proj", root, &[], &context, Some(&mut resolver))
2662 .expect("parse result")
2663 .expect("parse file");
2664
2665 let call = parsed
2666 .calls
2667 .iter()
2668 .find(|call| call.callee_name == "Tooltip")
2669 .expect("Tooltip call");
2670 assert_eq!(call.callee_target_kind.as_str(), "external");
2671 assert_eq!(
2672 call.callee_external_module.as_deref(),
2673 Some("package:flutter/material.dart")
2674 );
2675 assert!(resolver.requests.iter().any(|request| {
2676 request.language == "dart"
2677 && request.file_path == path
2678 && request.root_path == root
2679 && request.callee_name == "Tooltip"
2680 }));
2681 }
2682
2683 #[test]
2684 fn semantic_resolver_receives_utf16_columns_for_ast_calls() {
2685 let tempdir = TempDir::new().expect("create tempdir");
2686 let root = tempdir.path();
2687 let path = root.join("src/main.cpp");
2688 fs::create_dir_all(path.parent().expect("parent")).expect("create parent dirs");
2689 let source = format!(
2690 "void run() {{\n auto s = \"{}\"; printf(\"x\");\n}}\n",
2691 '\u{1F600}'
2692 );
2693 fs::write(&path, source.as_bytes()).expect("write source");
2694 let candidates = discover_supported_files(root);
2695 let context = build_import_resolution_context(root, &candidates);
2696 let mut resolver = FakeSemanticResolver {
2697 target: None,
2698 expected_language: "cpp",
2699 expected_callee: "printf",
2700 requests: Vec::new(),
2701 error: None,
2702 };
2703
2704 parse_file_with_semantic(&path, "proj", root, &[], &context, Some(&mut resolver))
2705 .expect("parse result")
2706 .expect("parse file");
2707
2708 let request = resolver
2709 .requests
2710 .iter()
2711 .find(|request| request.callee_name == "printf")
2712 .expect("printf semantic request");
2713 let prefix = format!(" auto s = \"{}\"; ", '\u{1F600}');
2714 assert_eq!(request.line, 2);
2715 assert_eq!(request.column, prefix.encode_utf16().count());
2716 }
2717
2718 #[test]
2719 fn semantic_resolver_receives_utf16_columns_for_textual_dart_calls() {
2720 let tempdir = TempDir::new().expect("create tempdir");
2721 let root = tempdir.path();
2722 let path = root.join("lib/sample.dart");
2723 fs::create_dir_all(path.parent().expect("parent")).expect("create parent dirs");
2724 let source = format!(
2725 "void run() {{\n final s = '{}'; Tooltip(message: 'x');\n}}\n",
2726 '\u{1F600}'
2727 );
2728 fs::write(&path, source.as_bytes()).expect("write source");
2729 let candidates = discover_supported_files(root);
2730 let context = build_import_resolution_context(root, &candidates);
2731 let mut resolver = FakeSemanticResolver {
2732 target: None,
2733 expected_language: "dart",
2734 expected_callee: "Tooltip",
2735 requests: Vec::new(),
2736 error: None,
2737 };
2738
2739 parse_file_with_semantic(&path, "proj", root, &[], &context, Some(&mut resolver))
2740 .expect("parse result")
2741 .expect("parse file");
2742
2743 let request = resolver
2744 .requests
2745 .iter()
2746 .find(|request| request.callee_name == "Tooltip")
2747 .expect("Tooltip semantic request");
2748 let prefix = format!(" final s = '{}'; ", '\u{1F600}');
2749 assert_eq!(request.line, 2);
2750 assert_eq!(request.column, prefix.encode_utf16().count());
2751 }
2752
2753 #[test]
2754 fn semantic_resolver_errors_are_propagated() {
2755 let tempdir = TempDir::new().expect("create tempdir");
2756 let root = tempdir.path();
2757 let path = root.join("src/main.cpp");
2758 fs::create_dir_all(path.parent().expect("parent")).expect("create parent dirs");
2759 fs::write(
2760 &path,
2761 r#"
2762void run() {
2763 printf("x");
2764}
2765"#,
2766 )
2767 .expect("write source");
2768 let candidates = discover_supported_files(root);
2769 let context = build_import_resolution_context(root, &candidates);
2770 let mut resolver = FakeSemanticResolver {
2771 target: None,
2772 expected_language: "cpp",
2773 expected_callee: "printf",
2774 requests: Vec::new(),
2775 error: Some("semantic resolver failed"),
2776 };
2777
2778 let err =
2779 match parse_file_with_semantic(&path, "proj", root, &[], &context, Some(&mut resolver))
2780 {
2781 Err(err) => err,
2782 Ok(_) => panic!("expected semantic resolver error"),
2783 };
2784
2785 assert_eq!(err.to_string(), "semantic resolver failed");
2786 }
2787
2788 #[test]
2789 fn classifies_external_swift_module_qualified_calls() {
2790 let parsed = parse_swift(
2791 r#"
2792import Foundation
2793
2794func run() {
2795 Foundation.Date()
2796}
2797"#,
2798 &[],
2799 );
2800
2801 let call = parsed.calls.first().expect("call");
2802 assert_eq!(call.callee_target_kind.as_str(), "external");
2803 assert_eq!(call.callee_name, "Date");
2804 assert_eq!(call.callee_external_module.as_deref(), Some("Foundation"));
2805 }
2806
2807 #[test]
2808 fn classifies_external_swift_scoped_import_module_qualified_calls() {
2809 let parsed = parse_swift(
2810 r#"
2811import struct Foundation.Date
2812
2813func run() {
2814 Foundation.Date()
2815}
2816"#,
2817 &[],
2818 );
2819
2820 let call = parsed.calls.first().expect("call");
2821 assert_eq!(call.callee_target_kind.as_str(), "external");
2822 assert_eq!(call.callee_name, "Date");
2823 assert_eq!(call.callee_external_module.as_deref(), Some("Foundation"));
2824 }
2825
2826 #[test]
2827 fn leaves_swift_unqualified_and_member_calls_unresolved() {
2828 let parsed = parse_swift(
2829 r#"
2830import Foundation
2831
2832func run(date: Date) {
2833 Date()
2834 date.formatted()
2835}
2836"#,
2837 &[],
2838 );
2839
2840 assert_eq!(parsed.calls.len(), 2);
2841 assert!(
2842 parsed
2843 .calls
2844 .iter()
2845 .all(|call| call.callee_target_kind.as_str() == "unresolved")
2846 );
2847 }
2848}