1use std::collections::{BTreeMap, BTreeSet};
11use std::fmt;
12
13use serde::{Deserialize, Serialize};
14use tree_sitter::Parser;
15
16use super::facts::{BlobKey, FactPaths, ManifestFacts, ProjectFacts};
17use crate::callgraph::{self, FileCallData, SymbolMeta};
18use crate::imports::{ImportBlock, ImportForm, ImportGroup, ImportKind, ImportStatement};
19use crate::parser::{grammar_for, LangId};
20use crate::symbols::SymbolKind;
21use crate::views::{Manifest, ManifestEntry, RelPath};
22use std::collections::HashMap;
23use std::path::Path;
24use std::rc::Rc;
25use std::sync::Arc;
26
27const TOP_LEVEL_SYMBOL: &str = "<top-level>";
28
29#[derive(Clone, Debug, Eq, PartialEq)]
31pub enum ManifestJoinError {
32 UnsupportedLanguage(String),
33 Parse(String),
34 InvalidBlob(String),
35 MissingBlob(String),
36 InvalidConfig { path: Vec<u8>, reason: String },
37}
38
39impl fmt::Display for ManifestJoinError {
40 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
41 match self {
42 Self::UnsupportedLanguage(language) => {
43 write!(formatter, "unsupported callgraph blob language {language}")
44 }
45 Self::Parse(reason) => write!(formatter, "callgraph blob parse failed: {reason}"),
46 Self::InvalidBlob(reason) => write!(formatter, "invalid callgraph blob: {reason}"),
47 Self::MissingBlob(key) => write!(
48 formatter,
49 "manifest references missing callgraph blob {key}"
50 ),
51 Self::InvalidConfig { path, reason } => write!(
52 formatter,
53 "invalid manifest config {}: {reason}",
54 String::from_utf8_lossy(path)
55 ),
56 }
57 }
58}
59
60impl std::error::Error for ManifestJoinError {}
61
62pub trait ManifestBlobReader {
67 fn read_callgraph_blob(&self, full_key: &str) -> Result<Option<Vec<u8>>, ManifestJoinError>;
68
69 fn read_callgraph_blob_decoded(
70 &self,
71 full_key: &str,
72 ) -> Result<Option<Arc<CallgraphBlob>>, ManifestJoinError> {
73 self.read_callgraph_blob(full_key)?
74 .map(|payload| CallgraphBlob::from_bytes(&payload).map(Arc::new))
75 .transpose()
76 }
77}
78
79#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
81pub struct AstPreorderNode {
82 pub ordinal: u32,
83 pub kind: String,
84 pub byte_start: usize,
85 pub byte_end: usize,
86}
87
88#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
91pub struct BlobSymbol {
92 pub ordinal: u32,
93 pub name: String,
94 pub scoped_name: String,
95 pub kind: String,
96 pub exported: bool,
97 pub is_default_export: bool,
98 pub start_line: u32,
99 pub start_col: u32,
100 pub end_line: u32,
101 pub end_col: u32,
102 pub signature: Option<String>,
103}
104
105#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
107#[serde(rename_all = "snake_case")]
108pub enum BlobRefKind {
109 Call,
110 ValueRef,
111 Import,
112 Module,
113 Reexport,
114 ExportAlias,
115}
116
117#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
119pub struct BlobRef {
120 pub ordinal: u32,
122 pub kind: BlobRefKind,
123 pub caller_symbol: Option<String>,
124 pub short_name: Option<String>,
125 pub full_ref: Option<String>,
126 pub module_path: Option<String>,
127 pub line: u32,
128 pub byte_start: usize,
129 pub byte_end: usize,
130 pub path_override: Option<String>,
131 pub local_name: Option<String>,
132 pub requested_name: Option<String>,
133 pub namespace_alias: Option<String>,
134 pub wildcard: bool,
135 pub import_kind: Option<String>,
136}
137
138#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
141pub struct BlobImport {
142 pub ordinal: u32,
143 pub module_path: String,
144 pub names: Vec<String>,
145 pub default_import: Option<String>,
146 pub namespace_import: Option<String>,
147 pub byte_start: usize,
148 pub byte_end: usize,
149 pub raw_text: String,
150 pub type_only: bool,
151 pub side_effect: bool,
152}
153
154#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
156pub struct ParseBlob {
157 pub extractor_version: String,
158 pub language: String,
159 pub ast_nodes: Vec<AstPreorderNode>,
160 pub symbols: Vec<BlobSymbol>,
161 pub default_export_symbol: Option<String>,
162 pub exported_symbols: Vec<String>,
163 pub callable_symbols: Vec<String>,
164 pub imports: Vec<BlobImport>,
165 pub refs: Vec<BlobRef>,
166}
167
168#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
172pub struct ConfigBlob {
173 pub extractor_version: String,
174 pub language: String,
175 pub source: Vec<u8>,
176}
177
178#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
182#[serde(tag = "kind", rename_all = "snake_case")]
183pub enum CallgraphBlob {
184 Parse(ParseBlob),
185 Config(ConfigBlob),
186}
187
188impl CallgraphBlob {
189 pub fn extract(
192 source: &str,
193 language: &str,
194 extractor_version: impl Into<String>,
195 ) -> Result<Self, ManifestJoinError> {
196 let lang = language_id(language)
197 .ok_or_else(|| ManifestJoinError::UnsupportedLanguage(language.to_string()))?;
198 let extractor_version = extractor_version.into();
199 let ast_nodes = ast_preorder_nodes(source, lang)?;
200 let mut data = callgraph::build_file_data_from_source_with_lang(
201 std::path::Path::new("__callgraph_blob__"),
202 source,
203 lang,
204 )
205 .map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
206 if lang == LangId::Rust {
207 super::extend_rust_imports_with_nested_uses(source, &mut data);
208 }
209 let symbols = blob_symbols(source, &data, &ast_nodes);
210 let imports = blob_imports(&data, &ast_nodes);
211 let mut refs = blob_refs(source, &data, &ast_nodes);
212 refs.extend(rust_module_refs(source, lang, &ast_nodes));
213 let empty =
214 Manifest::new([]).map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
215 let reader = |_: &BlobKey| None;
216 let empty_facts = ManifestFacts {
217 manifest: &empty,
218 blobs: &reader,
219 };
220 let paths = FactPaths {
221 root: Path::new("/"),
222 facts: &empty_facts,
223 };
224 let file = Path::new("/__callgraph_blob__");
225 let mut structural =
226 super::collect_reexport_refs(paths.root, file, "__callgraph_blob__", source, &paths)
227 .raw_refs;
228 structural.extend(
229 super::collect_source_less_export_alias_refs("__callgraph_blob__", source).raw_refs,
230 );
231 if lang == LangId::Rust {
232 structural.extend(
233 super::collect_rust_pub_use_reexport_refs(
234 paths.root,
235 file,
236 "__callgraph_blob__",
237 &data.import_block.imports,
238 &super::LineIndex::new(source),
239 &paths,
240 )
241 .raw_refs,
242 );
243 }
244 refs.extend(
245 structural
246 .into_iter()
247 .map(|raw| structural_ref(raw, &ast_nodes)),
248 );
249 let mut exported_symbols = data.exported_symbols.clone();
250 exported_symbols.sort();
251 let mut callable_symbols = data.calls_by_symbol.keys().cloned().collect::<Vec<_>>();
252 callable_symbols.sort();
253 refs.sort_by(|left, right| {
254 (
255 left.ordinal,
256 left.kind,
257 left.byte_start,
258 left.byte_end,
259 left.full_ref.as_deref(),
260 )
261 .cmp(&(
262 right.ordinal,
263 right.kind,
264 right.byte_start,
265 right.byte_end,
266 right.full_ref.as_deref(),
267 ))
268 });
269 refs.dedup_by(|left, right| {
270 left.ordinal == right.ordinal
271 && left.kind == right.kind
272 && left.byte_start == right.byte_start
273 && left.byte_end == right.byte_end
274 && left.full_ref == right.full_ref
275 });
276
277 Ok(Self::Parse(ParseBlob {
278 extractor_version,
279 language: language.to_string(),
280 ast_nodes,
281 symbols,
282 default_export_symbol: data.default_export_symbol,
283 exported_symbols,
284 callable_symbols,
285 imports,
286 refs,
287 }))
288 }
289
290 pub fn config(source: impl Into<Vec<u8>>, extractor_version: impl Into<String>) -> Self {
293 Self::Config(ConfigBlob {
294 extractor_version: extractor_version.into(),
295 language: "config".to_string(),
296 source: source.into(),
297 })
298 }
299
300 pub fn to_bytes(&self) -> Result<Vec<u8>, ManifestJoinError> {
302 serde_json::to_vec(self).map_err(|error| ManifestJoinError::InvalidBlob(error.to_string()))
303 }
304
305 pub fn from_bytes(bytes: &[u8]) -> Result<Self, ManifestJoinError> {
307 serde_json::from_slice(bytes)
308 .map_err(|error| ManifestJoinError::InvalidBlob(error.to_string()))
309 }
310
311 pub fn parse(&self) -> Option<&ParseBlob> {
312 match self {
313 Self::Parse(blob) => Some(blob),
314 Self::Config(_) => None,
315 }
316 }
317
318 pub fn config_source(&self) -> Option<&ConfigBlob> {
319 match self {
320 Self::Parse(_) => None,
321 Self::Config(blob) => Some(blob),
322 }
323 }
324}
325
326#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
329pub struct CallerRefKey {
330 pub caller_blob_key: String,
331 pub ref_ordinal: u32,
332 pub caller_path: Vec<u8>,
333}
334
335#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
337pub enum ResolutionStatus {
338 Resolved,
339 Unresolved,
340}
341
342#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
345pub struct DerivedRow {
346 pub caller_blob_key: String,
347 pub ref_ordinal: u32,
348 pub caller_path: Vec<u8>,
349 pub kind: BlobRefKind,
350 pub status: ResolutionStatus,
351 pub target_path: Option<Vec<u8>>,
352 pub target_symbol: Option<String>,
353}
354
355impl DerivedRow {
356 pub fn ref_key(&self) -> CallerRefKey {
357 CallerRefKey {
358 caller_blob_key: self.caller_blob_key.clone(),
359 ref_ordinal: self.ref_ordinal,
360 caller_path: self.caller_path.clone(),
361 }
362 }
363}
364
365#[derive(Clone, Debug, Eq, PartialEq)]
368pub struct JoinResult {
369 pub rows: BTreeSet<DerivedRow>,
370 pub resolution_order: Vec<CallerRefKey>,
371 pub unbound_non_utf8_paths: Vec<Vec<u8>>,
372}
373
374impl JoinResult {
375 pub fn canonical_serialization(&self) -> Vec<u8> {
378 let mut output = Vec::new();
379 for row in &self.rows {
380 append_field(&mut output, row.caller_blob_key.as_bytes());
381 append_field(&mut output, &row.ref_ordinal.to_be_bytes());
382 append_field(&mut output, &row.caller_path);
383 append_field(&mut output, &[row.kind as u8]);
384 append_field(
385 &mut output,
386 &[match row.status {
387 ResolutionStatus::Resolved => 1,
388 ResolutionStatus::Unresolved => 0,
389 }],
390 );
391 append_optional_field(&mut output, row.target_path.as_deref());
392 append_optional_field(&mut output, row.target_symbol.as_deref().map(str::as_bytes));
393 }
394 output
395 }
396}
397
398#[derive(Clone, Debug, Eq, PartialEq)]
400pub struct IncrementalJoinResult {
401 pub result: JoinResult,
402 pub re_resolved: BTreeSet<CallerRefKey>,
403 pub full_re_resolve: bool,
404}
405
406fn changed_manifest_paths(previous: &Manifest, current: &Manifest) -> BTreeSet<Vec<u8>> {
407 let previous_entries = previous
408 .entries()
409 .map(|(path, entry)| (path.as_bytes().to_vec(), entry))
410 .collect::<BTreeMap<_, _>>();
411 let current_entries = current
412 .entries()
413 .map(|(path, entry)| (path.as_bytes().to_vec(), entry))
414 .collect::<BTreeMap<_, _>>();
415 previous_entries
416 .keys()
417 .chain(current_entries.keys())
418 .collect::<BTreeSet<_>>()
419 .into_iter()
420 .filter(|path| previous_entries.get(*path) != current_entries.get(*path))
421 .cloned()
422 .collect()
423}
424
425fn manifest_resolution_input(manifest: &Manifest, path: &[u8]) -> bool {
426 let lookup = if path.first() == Some(&0) {
427 return manifest
428 .entries()
429 .find(|(candidate, _)| candidate.as_bytes() == path)
430 .is_some_and(|(_, entry)| matches!(entry, ManifestEntry::Synthetic { .. }));
431 } else {
432 RelPath::new(path.to_vec()).ok()
433 };
434 lookup
435 .as_ref()
436 .and_then(|path| manifest.get(path))
437 .is_some_and(|entry| {
438 matches!(
439 entry,
440 ManifestEntry::Regular {
441 resolution_input: true,
442 ..
443 }
444 )
445 })
446}
447
448fn append_field(output: &mut Vec<u8>, value: &[u8]) {
449 output.extend_from_slice(&(value.len() as u64).to_be_bytes());
450 output.extend_from_slice(value);
451}
452
453fn append_optional_field(output: &mut Vec<u8>, value: Option<&[u8]>) {
454 match value {
455 Some(value) => {
456 output.push(1);
457 append_field(output, value);
458 }
459 None => output.push(0),
460 }
461}
462
463fn language_id(language: &str) -> Option<LangId> {
464 Some(match language {
465 "typescript" => LangId::TypeScript,
466 "tsx" => LangId::Tsx,
467 "javascript" => LangId::JavaScript,
468 "python" => LangId::Python,
469 "rust" => LangId::Rust,
470 "go" => LangId::Go,
471 "c" => LangId::C,
472 "cpp" => LangId::Cpp,
473 "cuda" => LangId::Cuda,
474 "metal" => LangId::Metal,
475 "zig" => LangId::Zig,
476 "csharp" => LangId::CSharp,
477 "bash" => LangId::Bash,
478 "html" => LangId::Html,
479 "markdown" => LangId::Markdown,
480 "solidity" => LangId::Solidity,
481 "scss" => LangId::Scss,
482 "vue" => LangId::Vue,
483 "json" => LangId::Json,
484 "scala" => LangId::Scala,
485 "java" => LangId::Java,
486 "ruby" => LangId::Ruby,
487 "kotlin" => LangId::Kotlin,
488 "swift" => LangId::Swift,
489 "php" => LangId::Php,
490 "lua" => LangId::Lua,
491 "perl" => LangId::Perl,
492 "yaml" => LangId::Yaml,
493 "pascal" => LangId::Pascal,
494 "r" => LangId::R,
495 "groovy" => LangId::Groovy,
496 "objc" => LangId::ObjC,
497 "toml" => LangId::Toml,
498 _ => return None,
499 })
500}
501
502fn ast_preorder_nodes(
503 source: &str,
504 lang: LangId,
505) -> Result<Vec<AstPreorderNode>, ManifestJoinError> {
506 let mut parser = Parser::new();
507 parser
508 .set_language(&grammar_for(lang))
509 .map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
510 let tree = parser
511 .parse(source, None)
512 .ok_or_else(|| ManifestJoinError::Parse("tree-sitter returned no tree".to_string()))?;
513 let mut nodes = Vec::new();
514 let mut stack = vec![tree.root_node()];
515 while let Some(node) = stack.pop() {
516 nodes.push(AstPreorderNode {
517 ordinal: nodes.len() as u32,
518 kind: node.kind().to_string(),
519 byte_start: node.start_byte(),
520 byte_end: node.end_byte(),
521 });
522 let children = node.children(&mut node.walk()).collect::<Vec<_>>();
523 stack.extend(children.into_iter().rev());
524 }
525 Ok(nodes)
526}
527
528fn blob_symbols(
529 source: &str,
530 data: &FileCallData,
531 ast_nodes: &[AstPreorderNode],
532) -> Vec<BlobSymbol> {
533 let mut symbols = data
534 .symbol_metadata
535 .iter()
536 .map(|(scoped_name, meta)| {
537 blob_symbol(
538 source,
539 scoped_name,
540 meta,
541 &data.default_export_symbol,
542 ast_nodes,
543 )
544 })
545 .collect::<Vec<_>>();
546 symbols.sort_by(|left, right| {
547 (
548 left.ordinal,
549 left.start_line,
550 left.start_col,
551 left.scoped_name.as_str(),
552 )
553 .cmp(&(
554 right.ordinal,
555 right.start_line,
556 right.start_col,
557 right.scoped_name.as_str(),
558 ))
559 });
560 symbols
561}
562
563fn blob_symbol(
564 source: &str,
565 scoped_name: &str,
566 meta: &SymbolMeta,
567 default_export: &Option<String>,
568 ast_nodes: &[AstPreorderNode],
569) -> BlobSymbol {
570 let byte_start = byte_offset(source, meta.range.start_line, meta.range.start_col);
571 let byte_end = byte_offset(source, meta.range.end_line, meta.range.end_col).max(byte_start);
572 BlobSymbol {
573 ordinal: ordinal_for_range(ast_nodes, byte_start, byte_end),
574 name: unqualified_symbol_name(scoped_name).to_string(),
575 scoped_name: scoped_name.to_string(),
576 kind: symbol_kind_name(&meta.kind).to_string(),
577 exported: meta.exported,
578 is_default_export: default_export.as_deref() == Some(scoped_name),
579 start_line: meta.range.start_line,
580 start_col: meta.range.start_col,
581 end_line: meta.range.end_line,
582 end_col: meta.range.end_col,
583 signature: meta.signature.clone(),
584 }
585}
586
587fn blob_imports(data: &FileCallData, ast_nodes: &[AstPreorderNode]) -> Vec<BlobImport> {
588 let mut imports = data
589 .import_block
590 .imports
591 .iter()
592 .map(|import| BlobImport {
593 ordinal: ordinal_for_range(ast_nodes, import.byte_range.start, import.byte_range.end),
594 module_path: import.module_path.clone(),
595 names: import.names.clone(),
596 default_import: import.default_import.clone(),
597 namespace_import: import.namespace_import.clone(),
598 byte_start: import.byte_range.start,
599 byte_end: import.byte_range.end,
600 raw_text: import.raw_text.clone(),
601 type_only: import.kind == ImportKind::Type,
602 side_effect: import.kind == ImportKind::SideEffect,
603 })
604 .collect::<Vec<_>>();
605 imports.sort_by(|left, right| {
606 (left.ordinal, left.module_path.as_str()).cmp(&(right.ordinal, right.module_path.as_str()))
607 });
608 imports
609}
610
611fn blob_refs(source: &str, data: &FileCallData, ast_nodes: &[AstPreorderNode]) -> Vec<BlobRef> {
612 let mut refs = Vec::new();
613 for (caller_symbol, calls) in &data.calls_by_symbol {
614 for call in calls {
615 refs.push(call_ref(caller_symbol, call, BlobRefKind::Call, ast_nodes));
616 }
617 }
618 for (caller_symbol, calls) in &data.value_refs_by_symbol {
619 for call in calls {
620 refs.push(call_ref(
621 caller_symbol,
622 call,
623 BlobRefKind::ValueRef,
624 ast_nodes,
625 ));
626 }
627 }
628 for import in &data.import_block.imports {
629 refs.push(BlobRef {
630 ordinal: ordinal_for_range(ast_nodes, import.byte_range.start, import.byte_range.end),
631 kind: BlobRefKind::Import,
632 caller_symbol: None,
633 short_name: None,
634 full_ref: Some(import.module_path.clone()),
635 module_path: Some(import.module_path.clone()),
636 line: line_for_byte(source, import.byte_range.start),
637 byte_start: import.byte_range.start,
638 byte_end: import.byte_range.end,
639 path_override: None,
640 local_name: None,
641 requested_name: None,
642 namespace_alias: import.namespace_import.clone(),
643 wildcard: super::import_is_wildcard(import),
644 import_kind: None,
645 });
646 }
647 refs
648}
649
650fn call_ref(
651 caller_symbol: &str,
652 call: &callgraph::CallSite,
653 kind: BlobRefKind,
654 ast_nodes: &[AstPreorderNode],
655) -> BlobRef {
656 BlobRef {
657 ordinal: ordinal_for_range(ast_nodes, call.byte_start, call.byte_end),
658 kind,
659 caller_symbol: Some(caller_symbol.to_string()),
660 short_name: Some(call.callee_name.clone()),
661 full_ref: Some(call.full_callee.clone()),
662 module_path: None,
663 line: call.line,
664 byte_start: call.byte_start,
665 byte_end: call.byte_end,
666 path_override: None,
667 local_name: None,
668 requested_name: None,
669 namespace_alias: None,
670 wildcard: false,
671 import_kind: None,
672 }
673}
674
675fn rust_module_refs(source: &str, lang: LangId, ast_nodes: &[AstPreorderNode]) -> Vec<BlobRef> {
676 if lang != LangId::Rust {
677 return Vec::new();
678 }
679 let mut parser = Parser::new();
680 if parser.set_language(&grammar_for(lang)).is_err() {
681 return Vec::new();
682 }
683 let Some(tree) = parser.parse(source, None) else {
684 return Vec::new();
685 };
686 let mut refs = Vec::new();
687 let mut stack = vec![tree.root_node()];
688 while let Some(node) = stack.pop() {
689 if node.kind() == "mod_item"
690 && node
691 .named_children(&mut node.walk())
692 .all(|child| child.kind() != "declaration_list")
693 {
694 if let Some(name) = node.child_by_field_name("name") {
695 let module_name = source[name.byte_range()].to_string();
696 refs.push(BlobRef {
697 ordinal: ordinal_for_range(ast_nodes, node.start_byte(), node.end_byte()),
698 kind: BlobRefKind::Module,
699 caller_symbol: None,
700 short_name: Some(module_name.clone()),
701 full_ref: Some(module_name.clone()),
702 module_path: Some(module_name),
703 line: node.start_position().row as u32 + 1,
704 byte_start: node.start_byte(),
705 byte_end: node.end_byte(),
706 path_override: super::rust_module_path_override(source, node)
707 .map(str::to_string),
708 local_name: None,
709 requested_name: None,
710 namespace_alias: None,
711 wildcard: false,
712 import_kind: None,
713 });
714 }
715 }
716 let children = node.children(&mut node.walk()).collect::<Vec<_>>();
717 stack.extend(children.into_iter().rev());
718 }
719 refs
720}
721
722fn ordinal_for_range(ast_nodes: &[AstPreorderNode], byte_start: usize, byte_end: usize) -> u32 {
723 ast_nodes
724 .iter()
725 .filter(|node| node.byte_start <= byte_start && node.byte_end >= byte_end)
726 .min_by_key(|node| (node.byte_end.saturating_sub(node.byte_start), node.ordinal))
727 .map(|node| node.ordinal)
728 .unwrap_or(0)
729}
730
731fn byte_offset(source: &str, line: u32, column: u32) -> usize {
732 let mut offset = 0usize;
733 for (index, segment) in source.split_inclusive('\n').enumerate() {
734 if index as u32 == line {
735 return offset + (column as usize).min(segment.len());
736 }
737 offset += segment.len();
738 }
739 source.len()
740}
741
742fn line_for_byte(source: &str, byte_start: usize) -> u32 {
743 source[..byte_start.min(source.len())]
744 .bytes()
745 .filter(|byte| *byte == b'\n')
746 .count() as u32
747 + 1
748}
749
750fn symbol_kind_name(kind: &SymbolKind) -> &'static str {
751 match kind {
752 SymbolKind::Function => "function",
753 SymbolKind::Kernel => "kernel",
754 SymbolKind::Class => "class",
755 SymbolKind::Method => "method",
756 SymbolKind::Struct => "struct",
757 SymbolKind::Interface => "interface",
758 SymbolKind::Enum => "enum",
759 SymbolKind::TypeAlias => "type_alias",
760 SymbolKind::Variable => "variable",
761 SymbolKind::Heading => "heading",
762 SymbolKind::FileSummary => "file_summary",
763 }
764}
765
766fn unqualified_symbol_name(scoped_name: &str) -> &str {
767 if scoped_name == TOP_LEVEL_SYMBOL {
768 return scoped_name;
769 }
770 scoped_name.rsplit("::").next().unwrap_or(scoped_name)
771}
772
773#[cfg(test)]
774mod tests {
775 use super::*;
776 #[test]
777 fn blob_ordinals_are_tree_sitter_preorder_positions() {
778 let source = "export function run() { return helper(); }\nfunction helper() {}\n";
779 let blob = CallgraphBlob::extract(source, "typescript", "join-test-v1").unwrap();
780 let repeated = CallgraphBlob::extract(source, "typescript", "join-test-v1").unwrap();
781 let different_version =
782 CallgraphBlob::extract(source, "typescript", "join-test-v2").unwrap();
783 assert_eq!(blob.to_bytes().unwrap(), repeated.to_bytes().unwrap());
784 assert_ne!(
785 blob.to_bytes().unwrap(),
786 different_version.to_bytes().unwrap()
787 );
788 let parse = blob.parse().unwrap();
789 assert_eq!(parse.ast_nodes[0].ordinal, 0);
790 assert!(parse.refs.iter().all(|reference| parse
791 .ast_nodes
792 .iter()
793 .any(|node| node.ordinal == reference.ordinal)));
794 let helper = parse
795 .refs
796 .iter()
797 .find(|reference| reference.short_name.as_deref() == Some("helper"))
798 .unwrap();
799 let node = parse
800 .ast_nodes
801 .iter()
802 .find(|node| node.ordinal == helper.ordinal)
803 .unwrap();
804 assert!(node.byte_start <= helper.byte_start && node.byte_end >= helper.byte_end);
805 assert_eq!(node.kind, "call_expression");
806 let mut parser = Parser::new();
807 parser
808 .set_language(&grammar_for(LangId::TypeScript))
809 .unwrap();
810 let tree = parser.parse(source, None).unwrap();
811 let mut cursor = tree.walk();
812 let mut expected = Vec::new();
813 'preorder: loop {
814 let node = cursor.node();
815 expected.push(AstPreorderNode {
816 ordinal: expected.len() as u32,
817 kind: node.kind().to_string(),
818 byte_start: node.start_byte(),
819 byte_end: node.end_byte(),
820 });
821 if cursor.goto_first_child() {
822 continue;
823 }
824 while !cursor.goto_next_sibling() {
825 if !cursor.goto_parent() {
826 break 'preorder;
827 }
828 }
829 }
830 assert_eq!(parse.ast_nodes, expected);
831 }
832}
833
834fn structural_ref(raw: super::RawRef, nodes: &[AstPreorderNode]) -> BlobRef {
835 BlobRef {
836 ordinal: ordinal_for_range(nodes, raw.byte_start, raw.byte_end),
837 kind: if raw.kind == "export_alias" {
838 BlobRefKind::ExportAlias
839 } else {
840 BlobRefKind::Reexport
841 },
842 caller_symbol: raw.caller_symbol,
843 short_name: raw.short_name,
844 full_ref: raw.full_ref,
845 module_path: raw.module_path,
846 line: raw.line,
847 byte_start: raw.byte_start,
848 byte_end: raw.byte_end,
849 path_override: None,
850 local_name: raw.local_name,
851 requested_name: raw.requested_name,
852 namespace_alias: raw.namespace_alias,
853 wildcard: raw.wildcard,
854 import_kind: raw.import_kind,
855 }
856}
857
858fn bound_name(name: &str, path: &str) -> String {
859 name.replace(
860 "<default:__callgraph_blob__>",
861 &format!(
862 "<default:{}>",
863 Path::new(path)
864 .file_name()
865 .unwrap_or_default()
866 .to_string_lossy()
867 ),
868 )
869}
870
871impl ParseBlob {
872 fn file_data(&self, path: &str) -> Result<FileCallData, ManifestJoinError> {
873 let lang = language_id(&self.language)
874 .ok_or_else(|| ManifestJoinError::UnsupportedLanguage(self.language.clone()))?;
875 let mut calls_by_symbol: HashMap<String, Vec<callgraph::CallSite>> = HashMap::new();
876 let mut value_refs_by_symbol: HashMap<String, Vec<callgraph::CallSite>> = HashMap::new();
877 for raw in &self.refs {
878 let map = match raw.kind {
879 BlobRefKind::Call => &mut calls_by_symbol,
880 BlobRefKind::ValueRef => &mut value_refs_by_symbol,
881 _ => continue,
882 };
883 let Some(caller) = &raw.caller_symbol else {
884 continue;
885 };
886 map.entry(bound_name(caller, path))
887 .or_default()
888 .push(callgraph::CallSite {
889 callee_name: raw.short_name.clone().unwrap_or_default(),
890 full_callee: raw.full_ref.clone().unwrap_or_default(),
891 line: raw.line,
892 byte_start: raw.byte_start,
893 byte_end: raw.byte_end,
894 });
895 }
896 for symbol in &self.callable_symbols {
897 calls_by_symbol.entry(bound_name(symbol, path)).or_default();
898 }
899 let mut symbol_metadata = HashMap::new();
900 for symbol in &self.symbols {
901 let kind = match symbol.kind.as_str() {
902 "function" => SymbolKind::Function,
903 "method" => SymbolKind::Method,
904 "class" => SymbolKind::Class,
905 "struct" => SymbolKind::Struct,
906 "interface" => SymbolKind::Interface,
907 "enum" => SymbolKind::Enum,
908 "type_alias" => SymbolKind::TypeAlias,
909 "heading" => SymbolKind::Heading,
910 "file_summary" => SymbolKind::FileSummary,
911 _ => SymbolKind::Variable,
912 };
913 symbol_metadata.insert(
914 bound_name(&symbol.scoped_name, path),
915 SymbolMeta {
916 kind,
917 exported: symbol.exported,
918 signature: symbol.signature.clone(),
919 line: symbol.start_line + 1,
920 range: crate::symbols::Range {
921 start_line: symbol.start_line,
922 start_col: symbol.start_col,
923 end_line: symbol.end_line,
924 end_col: symbol.end_col,
925 },
926 entry_point_attribute: None,
927 },
928 );
929 }
930 let imports = self
931 .imports
932 .iter()
933 .map(|import| ImportStatement {
934 module_path: import.module_path.clone(),
935 names: import.names.clone(),
936 default_import: import.default_import.clone(),
937 namespace_import: import.namespace_import.clone(),
938 kind: if import.type_only {
939 ImportKind::Type
940 } else if import.side_effect {
941 ImportKind::SideEffect
942 } else {
943 ImportKind::Value
944 },
945 group: ImportGroup::Internal,
946 byte_range: import.byte_start..import.byte_end,
947 raw_text: import.raw_text.clone(),
948 form: if lang == LangId::Rust {
949 ImportForm::RustUse {
950 visibility: import.default_import.clone(),
951 named: import.names.clone(),
952 }
953 } else {
954 ImportForm::Es {
955 default_import: import.default_import.clone(),
956 namespace_import: import.namespace_import.clone(),
957 named: import.names.clone(),
958 type_only: import.type_only,
959 side_effect: import.side_effect,
960 attribute_clause: None,
961 attribute_type: None,
962 }
963 },
964 })
965 .collect::<Vec<_>>();
966 Ok(FileCallData {
967 calls_by_symbol,
968 value_refs_by_symbol,
969 symbol_metadata,
970 exported_symbols: self
971 .exported_symbols
972 .iter()
973 .map(|s| bound_name(s, path))
974 .collect(),
975 default_export_symbol: self
976 .default_export_symbol
977 .as_ref()
978 .map(|s| bound_name(s, path)),
979 import_block: ImportBlock {
980 imports,
981 byte_range: None,
982 },
983 lang,
984 })
985 }
986
987 fn bind(
988 &self,
989 path: &str,
990 facts: &FactPaths<'_>,
991 ) -> Result<super::FileExtract, ManifestJoinError> {
992 self.bind_with_dependencies(path, facts, None)
993 }
994
995 fn bind_with_dependencies(
996 &self,
997 path: &str,
998 facts: &FactPaths<'_>,
999 cached: Option<&BTreeMap<u32, BTreeSet<String>>>,
1000 ) -> Result<super::FileExtract, ManifestJoinError> {
1001 let data = self.file_data(path)?;
1002 let nodes = self
1003 .symbols
1004 .iter()
1005 .map(|symbol| {
1006 let scoped_name = bound_name(&symbol.scoped_name, path);
1007 super::NodeRecord {
1008 id: format!("{path}:{}:{scoped_name}", symbol.ordinal),
1009 file_path: path.to_string(),
1010 name: bound_name(&symbol.name, path),
1011 scoped_name,
1012 kind: symbol.kind.clone(),
1013 range: crate::symbols::Range {
1014 start_line: symbol.start_line,
1015 start_col: symbol.start_col,
1016 end_line: symbol.end_line,
1017 end_col: symbol.end_col,
1018 },
1019 range_ordinal: symbol.ordinal,
1020 signature: symbol.signature.clone(),
1021 exported: symbol.exported,
1022 is_default_export: symbol.is_default_export,
1023 is_type_like: false,
1024 is_callgraph_entry_point: false,
1025 }
1026 })
1027 .collect::<Vec<_>>();
1028 let mut caller_nodes = HashMap::with_capacity(nodes.len());
1031 for node in &nodes {
1032 #[cfg(test)]
1033 CALLER_NODE_LOOKUP_WORK.with(|work| work.set(work.get() + 1));
1034 caller_nodes.entry(&node.scoped_name).or_insert(&node.id);
1035 }
1036 let abs = facts.root.join(path);
1037 let mut raw_refs = Vec::new();
1038 for (position, raw) in self.refs.iter().enumerate() {
1039 let position = u32::try_from(position).expect("reference vector fits u32");
1040 let dependencies =
1041 if let Some(dependencies) = cached.and_then(|cache| cache.get(&position)) {
1042 dependencies.clone()
1043 } else if raw.kind == BlobRefKind::Module {
1044 super::rust_external_module_target(
1045 &abs,
1046 raw.path_override.as_deref(),
1047 raw.module_path.as_deref().unwrap_or_default(),
1048 facts,
1049 )
1050 .and_then(|p| facts.canonical(&p))
1051 .map(|p| super::relative_path(facts.root, &p))
1052 .into_iter()
1053 .collect()
1054 } else if let Some(module) = &raw.module_path {
1055 super::module_dependencies(facts.root, &abs, module, facts)
1056 } else {
1057 BTreeSet::new()
1058 };
1059 let caller_symbol = raw
1060 .caller_symbol
1061 .as_ref()
1062 .map(|name| bound_name(name, path));
1063 let caller_node = caller_symbol.as_ref().and_then(|name| {
1064 #[cfg(test)]
1065 CALLER_NODE_LOOKUP_WORK.with(|work| work.set(work.get() + 1));
1066 #[cfg(test)]
1067 if SCAN_CALLER_NODES.with(std::cell::Cell::get) {
1068 return nodes
1069 .iter()
1070 .find(|node| {
1071 CALLER_NODE_LOOKUP_WORK.with(|work| work.set(work.get() + 1));
1072 &node.scoped_name == name
1073 })
1074 .map(|node| node.id.clone());
1075 }
1076 caller_nodes.get(name).map(|id| (*id).clone())
1077 });
1078 raw_refs.push(super::RawRef {
1079 ref_id: format!("{path}:{}:{:?}", raw.ordinal, raw.kind),
1080 caller_node,
1081 caller_symbol,
1082 caller_file: path.to_string(),
1083 kind: match raw.kind {
1084 BlobRefKind::Call => "call",
1085 BlobRefKind::ValueRef => "value_ref",
1086 BlobRefKind::Import => "import",
1087 BlobRefKind::Module => "module",
1088 BlobRefKind::Reexport => "reexport",
1089 BlobRefKind::ExportAlias => "export_alias",
1090 }
1091 .to_string(),
1092 short_name: raw.short_name.clone(),
1093 full_ref: raw.full_ref.clone(),
1094 module_path: raw.module_path.clone(),
1095 import_kind: raw.import_kind.clone(),
1096 local_name: raw.local_name.clone(),
1097 requested_name: raw.requested_name.clone(),
1098 namespace_alias: raw.namespace_alias.clone(),
1099 wildcard: raw.wildcard,
1100 line: raw.line,
1101 byte_start: raw.byte_start,
1102 byte_end: raw.byte_end,
1103 dependencies,
1104 });
1105 }
1106 Ok(super::FileExtract {
1107 rel_path: path.to_string(),
1108 freshness: crate::cache_freshness::FileFreshness {
1109 mtime: std::time::UNIX_EPOCH,
1110 size: 0,
1111 content_hash: crate::cache_freshness::zero_hash(),
1112 },
1113 lang: data.lang,
1114 data,
1115 nodes,
1116 raw_refs,
1117 dispatch_hints: Vec::new(),
1118 surface_fingerprint: String::new(),
1119 })
1120 }
1121}
1122
1123type ManifestProjectIndex<'a> = super::ProjectIndex<'a>;
1125
1126impl JoinResult {
1127 pub fn from_manifest(
1128 manifest: &Manifest,
1129 blobs: &impl ManifestBlobReader,
1130 ) -> Result<Self, ManifestJoinError> {
1131 let loaded = manifest_payloads(manifest, blobs)?;
1132 let reader = |key: &BlobKey| loaded.get(key).cloned();
1133 let facts = Rc::new(ManifestFacts {
1134 manifest,
1135 blobs: &reader,
1136 });
1137 Self::from_facts(manifest, &loaded, Path::new("/"), facts, None)
1138 }
1139
1140 fn from_facts<'a>(
1141 manifest: &'a Manifest,
1142 loaded: &BTreeMap<String, Arc<[u8]>>,
1143 root: &Path,
1144 facts: Rc<dyn ProjectFacts + 'a>,
1145 selected: Option<&BTreeSet<CallerRefKey>>,
1146 ) -> Result<Self, ManifestJoinError> {
1147 let paths = FactPaths {
1148 root,
1149 facts: facts.as_ref(),
1150 };
1151 let mut extracts = HashMap::new();
1152 let mut work = Vec::new();
1153 let mut unbound_non_utf8_paths = Vec::new();
1154 for (path, entry) in manifest.entries() {
1155 let ManifestEntry::Regular { planes, .. } = entry else {
1156 continue;
1157 };
1158 let Some(key) = &planes.callgraph else {
1159 continue;
1160 };
1161 let bytes = loaded
1162 .get(key)
1163 .ok_or_else(|| ManifestJoinError::MissingBlob(key.clone()))?;
1164 let CallgraphBlob::Parse(blob) = CallgraphBlob::from_bytes(bytes)? else {
1165 continue;
1166 };
1167 let Ok(rel) = std::str::from_utf8(path.as_bytes()) else {
1168 unbound_non_utf8_paths.push(path.as_bytes().to_vec());
1169 continue;
1170 };
1171 let extract = blob.bind(rel, &paths)?;
1172 for (raw, bound) in blob.refs.iter().zip(&extract.raw_refs) {
1173 let ref_key = CallerRefKey {
1174 caller_blob_key: key.clone(),
1175 ref_ordinal: raw.ordinal,
1176 caller_path: path.as_bytes().to_vec(),
1177 };
1178 if selected.is_none_or(|set| set.contains(&ref_key)) {
1179 work.push((ref_key, (raw.kind, bound.clone())));
1180 }
1181 }
1182 extracts.insert(rel.to_string(), extract);
1183 }
1184 let files = extracts
1185 .iter()
1186 .map(|(path, extract)| {
1187 (
1188 path.clone(),
1189 super::DbFileIndex::from_extract(root, extract, &paths),
1190 )
1191 })
1192 .collect();
1193 let caller_data = extracts
1194 .iter()
1195 .map(|(path, extract)| (path.clone(), &extract.data))
1196 .collect();
1197 let mut index = ManifestProjectIndex::from_parts(
1198 root,
1199 files,
1200 caller_data,
1201 super::WorkspaceCratePrefixCache::default(),
1202 facts,
1203 );
1204 index.unbound_non_utf8_paths = unbound_non_utf8_paths;
1205 if !index.unbound_non_utf8_paths.is_empty() {
1206 log::warn!(
1207 "callgraph index left {} non-UTF-8 source paths unbound",
1208 index.unbound_non_utf8_paths.len()
1209 );
1210 }
1211 let mut result = Self {
1212 rows: BTreeSet::new(),
1213 resolution_order: Vec::new(),
1214 unbound_non_utf8_paths: index.unbound_non_utf8_paths.clone(),
1215 };
1216 work.sort_by(|a, b| (&a.0, a.1 .0).cmp(&(&b.0, b.1 .0)));
1217 for (key, (kind, raw)) in work {
1218 let resolved = super::resolve_ref(raw, &index)
1219 .map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
1220 result.rows.insert(DerivedRow {
1221 caller_blob_key: key.caller_blob_key.clone(),
1222 ref_ordinal: key.ref_ordinal,
1223 caller_path: key.caller_path.clone(),
1224 kind,
1225 status: if resolved.target_file.is_some() {
1226 ResolutionStatus::Resolved
1227 } else {
1228 ResolutionStatus::Unresolved
1229 },
1230 target_path: resolved.target_file.map(String::into_bytes),
1231 target_symbol: resolved.target_symbol,
1232 });
1233 result.resolution_order.push(key);
1234 }
1235 Ok(result)
1236 }
1237
1238 pub fn update(
1239 &self,
1240 previous_manifest: &Manifest,
1241 manifest: &Manifest,
1242 blobs: &impl ManifestBlobReader,
1243 ) -> Result<IncrementalJoinResult, ManifestJoinError> {
1244 let changed = changed_manifest_paths(previous_manifest, manifest);
1245 let full_re_resolve = changed.iter().any(|path| {
1246 manifest_resolution_input(previous_manifest, path)
1247 || manifest_resolution_input(manifest, path)
1248 });
1249 let loaded = manifest_payloads(manifest, blobs)?;
1250 let reader = |key: &BlobKey| loaded.get(key).cloned();
1251 let mut current_keys = BTreeSet::new();
1252 for (path, entry) in manifest.entries() {
1253 if std::str::from_utf8(path.as_bytes()).is_err() {
1254 continue;
1255 }
1256 let ManifestEntry::Regular { planes, .. } = entry else {
1257 continue;
1258 };
1259 let Some(key) = &planes.callgraph else {
1260 continue;
1261 };
1262 if let CallgraphBlob::Parse(blob) = CallgraphBlob::from_bytes(&loaded[key])? {
1263 current_keys.extend(blob.refs.iter().map(|raw| CallerRefKey {
1264 caller_blob_key: key.clone(),
1265 ref_ordinal: raw.ordinal,
1266 caller_path: path.as_bytes().to_vec(),
1267 }));
1268 }
1269 }
1270 let previous_rows = self
1271 .rows
1272 .iter()
1273 .map(|row| (row.ref_key(), row))
1274 .collect::<BTreeMap<_, _>>();
1275 let selected = current_keys
1276 .iter()
1277 .filter(|key| {
1278 full_re_resolve
1279 || changed.contains(&key.caller_path)
1280 || previous_rows.get(*key).is_none_or(|row| {
1281 row.target_path
1282 .as_ref()
1283 .is_some_and(|path| changed.contains(path))
1284 })
1285 })
1286 .cloned()
1287 .collect::<BTreeSet<_>>();
1288 let facts = Rc::new(ManifestFacts {
1289 manifest,
1290 blobs: &reader,
1291 });
1292 let mut result =
1293 Self::from_facts(manifest, &loaded, Path::new("/"), facts, Some(&selected))?;
1294 result.rows.extend(
1295 self.rows
1296 .iter()
1297 .filter(|row| {
1298 current_keys.contains(&row.ref_key()) && !selected.contains(&row.ref_key())
1299 })
1300 .cloned(),
1301 );
1302 result.resolution_order = result.rows.iter().map(DerivedRow::ref_key).collect();
1303 result.resolution_order.sort();
1304 Ok(IncrementalJoinResult {
1305 result,
1306 re_resolved: selected,
1307 full_re_resolve,
1308 })
1309 }
1310}
1311
1312fn manifest_payloads(
1313 manifest: &Manifest,
1314 blobs: &impl ManifestBlobReader,
1315) -> Result<BTreeMap<String, Arc<[u8]>>, ManifestJoinError> {
1316 let mut loaded = BTreeMap::new();
1317 for (_, entry) in manifest.entries() {
1318 let ManifestEntry::Regular { planes, .. } = entry else {
1319 continue;
1320 };
1321 let Some(key) = &planes.callgraph else {
1322 continue;
1323 };
1324 if !loaded.contains_key(key) {
1325 let bytes = blobs
1326 .read_callgraph_blob(key)?
1327 .ok_or_else(|| ManifestJoinError::MissingBlob(key.clone()))?;
1328 loaded.insert(key.clone(), Arc::from(bytes));
1329 }
1330 }
1331 Ok(loaded)
1332}
1333
1334#[cfg(test)]
1335#[path = "../../tests/integration/join_manifest_test.rs"]
1336mod manifest_integration_tests;
1337
1338#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1341pub(crate) struct ViewBindingDependencies {
1342 pub references: BTreeMap<u32, BTreeSet<String>>,
1345 pub dependencies: BTreeSet<String>,
1346 #[serde(default)]
1347 pub consulted_facts: BTreeSet<(String, String)>,
1348 #[serde(default)]
1349 pub unattributed: bool,
1350 #[serde(default)]
1351 binding_facts: ConfigConsultations,
1352 #[serde(default)]
1353 resolution_facts: ConfigConsultations,
1354 binding_probes: BTreeSet<String>,
1355 resolved_dependencies: BTreeSet<String>,
1356 surface_queries: Vec<(ViewSurfaceQuery, String)>,
1357 #[serde(skip)]
1358 surface: Option<ViewFileSurface>,
1359}
1360
1361impl ViewBindingDependencies {
1362 pub(crate) fn from_surface_json(payload: &str) -> Result<Self, serde_json::Error> {
1363 Ok(Self {
1364 surface: Some(serde_json::from_str(payload)?),
1365 ..Self::default()
1366 })
1367 }
1368
1369 pub(crate) fn copy_surface_from(&mut self, source: &Self) {
1370 self.surface.clone_from(&source.surface);
1371 }
1372
1373 pub(crate) fn surface_json(&self) -> Result<Option<String>, serde_json::Error> {
1374 self.surface.as_ref().map(serde_json::to_string).transpose()
1375 }
1376}
1377
1378#[derive(Hash, PartialEq, Eq)]
1382struct ViewResolutionBinding {
1383 kind: String,
1384 full_ref: Option<String>,
1385 short_name: Option<String>,
1386 visible_rust_imports: usize,
1387}
1388
1389impl ViewResolutionBinding {
1390 fn new(raw: &super::RawRef, caller: &FileCallData) -> Self {
1391 Self {
1392 kind: raw.kind.clone(),
1393 full_ref: raw.full_ref.clone(),
1394 short_name: raw.short_name.clone(),
1395 visible_rust_imports: if caller.lang == LangId::Rust {
1396 caller
1397 .import_block
1398 .imports
1399 .partition_point(|import| import.byte_range.start <= raw.byte_start)
1400 } else {
1401 0
1402 },
1403 }
1404 }
1405}
1406
1407pub(crate) struct SelectedManifestJoin {
1408 pub result: JoinResult,
1409 pub bindings: BTreeMap<String, ViewBindingDependencies>,
1410 pub resolved_callers: BTreeSet<String>,
1411 pub rebuilt_surface_entries: usize,
1412 pub rebuilt_surface_paths: BTreeSet<String>,
1413 pub decoded_caller_blobs: usize,
1414 pub resolved_bindings: usize,
1415}
1416
1417#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
1422struct ViewFileSurface {
1423 language: String,
1424 exports: BTreeSet<String>,
1425 default_export: Option<String>,
1426 export_aliases: BTreeMap<String, String>,
1427 node_by_scoped: BTreeMap<String, String>,
1428 node_by_bare: BTreeMap<String, String>,
1429 node_kind_by_id: BTreeMap<String, String>,
1430 module_targets: BTreeMap<String, Option<String>>,
1431 declared_module_targets: BTreeMap<String, Option<String>>,
1432 reexports: Vec<(Option<String>, BTreeMap<String, String>, bool)>,
1433}
1434
1435impl ViewFileSurface {
1436 fn capture(language: &str, index: &super::DbFileIndex) -> Self {
1437 Self {
1438 language: language.into(),
1439 exports: index.exports.iter().cloned().collect(),
1440 default_export: index.default_export.clone(),
1441 export_aliases: index.export_aliases.clone().into_iter().collect(),
1442 node_by_scoped: index.node_by_scoped.clone().into_iter().collect(),
1443 node_by_bare: index.node_by_bare.clone().into_iter().collect(),
1444 node_kind_by_id: index.node_kind_by_id.clone().into_iter().collect(),
1445 module_targets: index.module_targets.clone().into_iter().collect(),
1446 declared_module_targets: index.declared_module_targets.clone().into_iter().collect(),
1447 reexports: index
1448 .reexports
1449 .iter()
1450 .map(|r| {
1451 (
1452 r.target_file.clone(),
1453 r.named.clone().into_iter().collect(),
1454 r.wildcard,
1455 )
1456 })
1457 .collect(),
1458 }
1459 }
1460
1461 fn restore(&self) -> super::DbFileIndex {
1462 super::DbFileIndex {
1463 lang: language_id(&self.language),
1464 exports: self.exports.iter().cloned().collect(),
1465 default_export: self.default_export.clone(),
1466 export_aliases: self.export_aliases.clone().into_iter().collect(),
1467 node_by_scoped: self.node_by_scoped.clone().into_iter().collect(),
1468 node_by_bare: self.node_by_bare.clone().into_iter().collect(),
1469 node_kind_by_id: self.node_kind_by_id.clone().into_iter().collect(),
1470 module_targets: self.module_targets.clone().into_iter().collect(),
1471 declared_module_targets: self.declared_module_targets.clone().into_iter().collect(),
1472 reexports: self
1473 .reexports
1474 .iter()
1475 .map(|(target_file, named, wildcard)| super::ReexportIndex {
1476 target_file: target_file.clone(),
1477 named: named.clone().into_iter().collect(),
1478 wildcard: *wildcard,
1479 })
1480 .collect(),
1481 }
1482 }
1483}
1484
1485pub(crate) fn view_resolution_config(path: &[u8]) -> bool {
1490 matches!(
1491 path.rsplit(|byte| *byte == b'/').next(),
1492 Some(b"package.json" | b"tsconfig.json" | b"pnpm-workspace.yaml" | b"Cargo.toml")
1493 )
1494}
1495
1496#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
1499struct ConfigConsultations {
1500 facts: BTreeSet<(String, String)>,
1501 reads: BTreeSet<String>,
1502 unattributed: bool,
1503}
1504impl ConfigConsultations {
1505 fn extend(&mut self, other: &Self) {
1506 self.facts.extend(other.facts.iter().cloned());
1507 self.reads.extend(other.reads.iter().cloned());
1508 self.unattributed |= other.unattributed;
1509 }
1510 fn unattributed(&self) -> bool {
1511 self.unattributed
1512 || self
1513 .reads
1514 .iter()
1515 .any(|path| !self.facts.iter().any(|(input, _)| input == path))
1516 }
1517}
1518
1519#[derive(Clone, Default)]
1520struct ConsultationTrace {
1521 config: ConfigConsultations,
1522 probes: BTreeSet<String>,
1523}
1524type ConsultationMemoKey = (std::path::PathBuf, String, String);
1525
1526struct ViewBindingFacts<'a> {
1529 inner: Rc<dyn ProjectFacts + 'a>,
1530 probes: std::cell::RefCell<BTreeSet<String>>,
1531 consultations: std::cell::RefCell<ConfigConsultations>,
1532 memo_stack: std::cell::RefCell<Vec<(ConsultationMemoKey, ConsultationTrace)>>,
1533 memo_traces: std::cell::RefCell<BTreeMap<ConsultationMemoKey, ConsultationTrace>>,
1534 workspace_packages:
1535 std::cell::RefCell<BTreeMap<(std::path::PathBuf, String), Option<std::path::PathBuf>>>,
1536 workspace_members:
1537 std::cell::RefCell<BTreeMap<std::path::PathBuf, Arc<Vec<std::path::PathBuf>>>>,
1538 canonical_cache: std::cell::RefCell<HashMap<Vec<u8>, Option<Vec<u8>>>>,
1539 file_cache: std::cell::RefCell<HashMap<Vec<u8>, bool>>,
1540 config_cache: std::cell::RefCell<HashMap<Vec<u8>, Option<Arc<[u8]>>>>,
1541 directory_cache: std::cell::RefCell<HashMap<Vec<u8>, Vec<super::facts::DirEntry>>>,
1542}
1543
1544impl<'a> ViewBindingFacts<'a> {
1545 fn new(inner: Rc<dyn ProjectFacts + 'a>) -> Self {
1546 Self {
1547 inner,
1548 probes: Default::default(),
1549 consultations: Default::default(),
1550 memo_stack: Default::default(),
1551 memo_traces: Default::default(),
1552 workspace_packages: Default::default(),
1553 workspace_members: Default::default(),
1554 canonical_cache: Default::default(),
1555 file_cache: Default::default(),
1556 config_cache: Default::default(),
1557 directory_cache: Default::default(),
1558 }
1559 }
1560 fn file_fact(&self, rel: &[u8]) -> bool {
1561 if let Some(value) = self.file_cache.borrow().get(rel) {
1562 return *value;
1563 }
1564 let value = self.inner.is_file(rel);
1565 self.file_cache.borrow_mut().insert(rel.to_vec(), value);
1566 value
1567 }
1568
1569 fn record(&self, path: &[u8]) {
1570 if view_resolution_config(path) {
1574 self.record(VIEW_CONFIG_MEMBERSHIP_DOMAIN.as_bytes());
1575 return;
1576 }
1577 if let Ok(path) = std::str::from_utf8(path) {
1578 let mut parts = Vec::new();
1579 for part in path.split('/') {
1580 match part {
1581 "" | "." => {}
1582 ".." => {
1583 parts.pop();
1584 }
1585 _ => parts.push(part),
1586 }
1587 }
1588 let path = parts.join("/");
1589 self.probes.borrow_mut().insert(path.clone());
1590 for (_, trace) in self.memo_stack.borrow_mut().iter_mut() {
1591 trace.probes.insert(path.clone());
1592 }
1593 }
1594 }
1595
1596 fn take_config(&self) -> ConfigConsultations {
1597 let mut result = std::mem::take(&mut *self.consultations.borrow_mut());
1598 result.unattributed = result.unattributed();
1599 result.reads.clear();
1600 result
1601 }
1602
1603 fn config_event(&self, path: &[u8], name: Option<&str>) {
1604 let Ok(path) = std::str::from_utf8(path) else {
1605 self.consultations.borrow_mut().unattributed = true;
1606 return;
1607 };
1608 let mut event = ConfigConsultations::default();
1609 if let Some(name) = name {
1610 event.facts.insert((path.into(), name.into()));
1611 } else {
1612 event.reads.insert(path.into());
1613 }
1614 self.consultations.borrow_mut().extend(&event);
1615 for (_, trace) in self.memo_stack.borrow_mut().iter_mut() {
1616 trace.config.extend(&event);
1617 }
1618 }
1619
1620 fn take(&self) -> BTreeSet<String> {
1621 std::mem::take(&mut *self.probes.borrow_mut())
1622 }
1623}
1624
1625impl ProjectFacts for ViewBindingFacts<'_> {
1626 fn records_config_facts(&self) -> bool {
1627 true
1628 }
1629 fn config_fact(&self, rel: &[u8], name: &str) {
1630 self.record(rel);
1631 if matches!(name, "workspaces" | "packages") {
1632 self.record(VIEW_CONFIG_MEMBERSHIP_DOMAIN.as_bytes());
1633 }
1634 self.config_event(rel, Some(name));
1635 }
1636 fn memo_start(&self, path: &Path, kind: &str, name: &str) {
1637 self.memo_stack.borrow_mut().push((
1638 (path.into(), kind.into(), name.into()),
1639 ConsultationTrace::default(),
1640 ));
1641 }
1642 fn memo_finish(&self, path: &Path, kind: &str, name: &str) {
1643 let (key, trace) = self
1644 .memo_stack
1645 .borrow_mut()
1646 .pop()
1647 .expect("balanced resolver memo recording");
1648 debug_assert_eq!(key, (path.into(), kind.into(), name.into()));
1649 self.memo_traces.borrow_mut().insert(key, trace);
1650 }
1651 fn memo_replay(&self, path: &Path, kind: &str, name: &str) {
1652 let traces = self.memo_traces.borrow();
1653 let Some(trace) = traces.get(&(path.into(), kind.into(), name.into())) else {
1654 self.consultations.borrow_mut().unattributed = true;
1655 return;
1656 };
1657 self.consultations.borrow_mut().extend(&trace.config);
1658 self.probes
1659 .borrow_mut()
1660 .extend(trace.probes.iter().cloned());
1661 for (_, parent) in self.memo_stack.borrow_mut().iter_mut() {
1662 parent.config.extend(&trace.config);
1663 parent.probes.extend(trace.probes.iter().cloned());
1664 }
1665 }
1666 fn workspace_package(&self, root: &Path, name: &str) -> Option<Option<std::path::PathBuf>> {
1667 self.workspace_packages
1668 .borrow()
1669 .get(&(root.into(), name.into()))
1670 .cloned()
1671 }
1672 fn remember_workspace_package(
1673 &self,
1674 root: &Path,
1675 name: &str,
1676 value: Option<std::path::PathBuf>,
1677 ) {
1678 self.workspace_packages
1679 .borrow_mut()
1680 .insert((root.into(), name.into()), value);
1681 }
1682 fn workspace_members(&self, root: &Path) -> Option<Arc<Vec<std::path::PathBuf>>> {
1683 self.workspace_members.borrow().get(root).cloned()
1684 }
1685 fn remember_workspace_members(&self, root: &Path, value: Arc<Vec<std::path::PathBuf>>) {
1686 self.workspace_members
1687 .borrow_mut()
1688 .insert(root.into(), value);
1689 }
1690 fn is_file(&self, rel: &[u8]) -> bool {
1691 let is_file = self.file_fact(rel);
1692 if is_file {
1693 self.record(rel);
1694 }
1695 is_file
1696 }
1697 fn is_dir(&self, rel: &[u8]) -> bool {
1698 self.inner.is_dir(rel)
1699 }
1700 fn config_bytes(&self, rel: &[u8]) -> Option<Arc<[u8]>> {
1701 self.consultations.borrow_mut().unattributed = true;
1702 for (_, trace) in self.memo_stack.borrow_mut().iter_mut() {
1703 trace.config.unattributed = true;
1704 }
1705 self.attributed_config_bytes(rel)
1706 }
1707 fn attributed_config_bytes(&self, rel: &[u8]) -> Option<Arc<[u8]>> {
1708 self.config_event(rel, None);
1709 self.record(rel);
1710 if let Some(value) = self.config_cache.borrow().get(rel) {
1711 return value.clone();
1712 }
1713 let value = self.inner.config_bytes(rel);
1714 self.config_cache
1715 .borrow_mut()
1716 .insert(rel.to_vec(), value.clone());
1717 value
1718 }
1719 fn symlink_target(&self, rel: &[u8]) -> Option<&[u8]> {
1720 self.inner.symlink_target(rel)
1721 }
1722 fn canonical(&self, rel: &[u8]) -> Option<Vec<u8>> {
1723 let cached = self.canonical_cache.borrow().get(rel).cloned();
1726 let canonical = cached.unwrap_or_else(|| {
1727 let value = self.inner.canonical(rel);
1728 self.canonical_cache
1729 .borrow_mut()
1730 .insert(rel.to_vec(), value.clone());
1731 value
1732 });
1733 if canonical.as_ref().is_none_or(|path| self.file_fact(path)) {
1737 self.record(rel);
1738 }
1739 canonical
1740 }
1741 fn list_dir(&self, rel: &[u8]) -> Vec<super::facts::DirEntry> {
1742 if let Some(value) = self.directory_cache.borrow().get(rel) {
1743 return value.clone();
1744 }
1745 let value = self.inner.list_dir(rel);
1746 self.directory_cache
1747 .borrow_mut()
1748 .insert(rel.to_vec(), value.clone());
1749 value
1750 }
1751}
1752
1753#[cfg(test)]
1758pub(crate) fn join_selected_manifest(
1759 manifest: &Manifest,
1760 blobs: &impl ManifestBlobReader,
1761 selected: Option<&BTreeSet<String>>,
1762 cached: &BTreeMap<String, ViewBindingDependencies>,
1763) -> Result<SelectedManifestJoin, ManifestJoinError> {
1764 join_manifest_with_surfaces(manifest, blobs, selected, cached, None)
1765}
1766
1767pub(crate) fn join_selected_manifest_reusing_surfaces(
1771 manifest: &Manifest,
1772 blobs: &impl ManifestBlobReader,
1773 selected: Option<&BTreeSet<String>>,
1774 cached: &BTreeMap<String, ViewBindingDependencies>,
1775 changed: &BTreeSet<String>,
1776 membership_changed: &BTreeSet<String>,
1777 fact_invalidated: &BTreeSet<String>,
1778) -> Result<SelectedManifestJoin, ManifestJoinError> {
1779 join_manifest_with_surfaces(
1780 manifest,
1781 blobs,
1782 selected,
1783 cached,
1784 Some((changed, membership_changed, fact_invalidated)),
1785 )
1786}
1787
1788fn join_manifest_with_surfaces(
1789 manifest: &Manifest,
1790 blobs: &impl ManifestBlobReader,
1791 selected: Option<&BTreeSet<String>>,
1792 cached: &BTreeMap<String, ViewBindingDependencies>,
1793 reuse: Option<(&BTreeSet<String>, &BTreeSet<String>, &BTreeSet<String>)>,
1794) -> Result<SelectedManifestJoin, ManifestJoinError> {
1795 let mut profile = crate::views::materialization::profile::PhaseTimer::new("join");
1796 let loaded = std::cell::RefCell::new(BTreeMap::<BlobKey, Arc<[u8]>>::new());
1797 let load = |key: &BlobKey| -> Result<Arc<[u8]>, ManifestJoinError> {
1798 if let Some(bytes) = loaded.borrow().get(key) {
1799 return Ok(bytes.clone());
1800 }
1801 let bytes: Arc<[u8]> = blobs
1802 .read_callgraph_blob(key)?
1803 .ok_or_else(|| ManifestJoinError::MissingBlob(key.clone()))?
1804 .into();
1805 loaded.borrow_mut().insert(key.clone(), bytes.clone());
1806 Ok(bytes)
1807 };
1808 let decoded = std::cell::RefCell::new(HashMap::<BlobKey, Arc<CallgraphBlob>>::new());
1809 let decode = |key: &BlobKey| -> Result<Arc<CallgraphBlob>, ManifestJoinError> {
1810 if let Some(blob) = decoded.borrow().get(key) {
1811 return Ok(blob.clone());
1812 }
1813 let blob = blobs
1814 .read_callgraph_blob_decoded(key)?
1815 .ok_or_else(|| ManifestJoinError::MissingBlob(key.clone()))?;
1816 decoded.borrow_mut().insert(key.clone(), blob.clone());
1817 Ok(blob)
1818 };
1819 let read_error = std::cell::RefCell::new(None);
1820 let reader = |key: &BlobKey| match load(key) {
1821 Ok(bytes) => Some(bytes),
1822 Err(error) => {
1823 *read_error.borrow_mut() = Some(error);
1824 None
1825 }
1826 };
1827 profile.finish("load_payloads");
1828 let facts = Rc::new(ViewBindingFacts::new(Rc::new(ManifestFacts {
1829 manifest,
1830 blobs: &reader,
1831 })));
1832 let root = Path::new("/");
1833 let paths = FactPaths {
1834 root,
1835 facts: facts.as_ref(),
1836 };
1837 let mut extracts = HashMap::new();
1838 let mut files = HashMap::new();
1839 let mut work: BTreeMap<String, Vec<_>> = BTreeMap::new();
1840 let mut bindings = BTreeMap::new();
1841 let mut unbound_non_utf8_paths = Vec::new();
1842 let mut rebuilt_surface_paths = BTreeSet::new();
1843 let mut decoded_caller_blobs = 0;
1844 let mut restored_surfaces = 0;
1845 let mut rebuilt_changed = 0;
1846 let mut rebuilt_facts = 0;
1847 let mut rebuilt_membership = 0;
1848 let mut rebuilt_missing = 0;
1849 for (path, entry) in manifest.entries() {
1850 let ManifestEntry::Regular { planes, .. } = entry else {
1851 continue;
1852 };
1853 let Some(key) = &planes.callgraph else {
1854 continue;
1855 };
1856 let Ok(rel) = std::str::from_utf8(path.as_bytes()) else {
1857 if matches!(decode(key)?.as_ref(), CallgraphBlob::Parse(_)) {
1858 unbound_non_utf8_paths.push(path.as_bytes().to_vec());
1859 }
1860 continue;
1861 };
1862 let resolve = selected.is_none_or(|set| set.contains(rel));
1863 let cache = cached.get(rel).filter(|cache| {
1864 if let Some((changed, membership, fact_invalidated)) = reuse {
1865 selected.is_some()
1866 && !changed.contains(rel)
1867 && !fact_invalidated.contains(rel)
1868 && cache.dependencies.is_disjoint(membership)
1869 } else {
1870 !resolve
1871 }
1872 });
1873 if let Some((cache, surface)) =
1874 cache.and_then(|cache| cache.surface.as_ref().map(|surface| (cache, surface)))
1875 {
1876 restored_surfaces += 1;
1877 files.insert(rel.to_string(), surface.restore());
1878 if resolve {
1879 bindings.insert(rel.to_string(), cache.clone());
1880 }
1881 continue;
1882 }
1883 let decoded_blob = decode(key)?;
1884 let CallgraphBlob::Parse(blob) = decoded_blob.as_ref() else {
1885 continue;
1886 };
1887 decoded_caller_blobs += 1;
1888 facts.take();
1889 facts.take_config();
1890 let extract =
1891 blob.bind_with_dependencies(rel, &paths, cache.map(|cache| &cache.references))?;
1892 let file_index = super::DbFileIndex::from_extract(root, &extract, &paths);
1893 rebuilt_surface_paths.insert(rel.to_string());
1894 match reuse {
1895 Some((changed, _, _)) if changed.contains(rel) => rebuilt_changed += 1,
1896 Some((_, _, invalidated)) if invalidated.contains(rel) => rebuilt_facts += 1,
1897 Some((_, membership, _))
1898 if cached
1899 .get(rel)
1900 .is_some_and(|old| !old.dependencies.is_disjoint(membership)) =>
1901 {
1902 rebuilt_membership += 1;
1903 }
1904 _ => rebuilt_missing += 1,
1905 }
1906 let mut binding = cache.cloned().unwrap_or_default();
1907 binding.surface = Some(ViewFileSurface::capture(&blob.language, &file_index));
1908 files.insert(rel.to_string(), file_index);
1909 if cache.is_none() {
1910 binding.references = blob
1911 .refs
1912 .iter()
1913 .zip(&extract.raw_refs)
1914 .enumerate()
1915 .map(|(position, (_, bound))| {
1916 (
1917 u32::try_from(position).expect("reference vector fits u32"),
1918 bound.dependencies.clone(),
1919 )
1920 })
1921 .collect();
1922 binding.binding_probes = facts.take();
1923 binding.binding_facts = facts.take_config();
1924 binding.dependencies = binding.references.values().flatten().cloned().collect();
1925 binding
1926 .dependencies
1927 .extend(binding.binding_probes.iter().cloned());
1928 }
1929 bindings.insert(rel.to_string(), binding);
1930 if resolve {
1931 let caller_work = work.entry(rel.to_string()).or_default();
1932 for (raw, bound) in blob.refs.iter().zip(&extract.raw_refs) {
1933 caller_work.push((
1934 CallerRefKey {
1935 caller_blob_key: key.clone(),
1936 ref_ordinal: raw.ordinal,
1937 caller_path: path.as_bytes().to_vec(),
1938 },
1939 (raw.kind, bound.clone()),
1940 ));
1941 }
1942 }
1943 extracts.insert(rel.to_string(), extract);
1944 }
1945 profile.finish("decode_bind_index_entries");
1946 let index = ManifestProjectIndex::from_parts(
1947 root,
1948 files,
1949 HashMap::new(),
1950 super::WorkspaceCratePrefixCache::default(),
1951 facts.clone(),
1952 );
1953 let mut result = JoinResult {
1954 rows: BTreeSet::new(),
1955 resolution_order: Vec::new(),
1956 unbound_non_utf8_paths,
1957 };
1958 let resolved_callers = bindings
1959 .keys()
1960 .filter(|path| {
1961 if selected.is_some_and(|set| !set.contains(*path)) {
1962 return false;
1963 }
1964 let Some((changed, _, _)) = reuse else {
1965 return true;
1966 };
1967 if selected.is_none() || changed.contains(*path) {
1968 return true;
1969 }
1970 cached.get(*path).is_none_or(|old| {
1971 old.references != bindings[*path].references
1972 || old
1973 .surface_queries
1974 .iter()
1975 .any(|(query, expected)| query.answer(&index) != *expected)
1976 })
1977 })
1978 .cloned()
1979 .collect::<BTreeSet<_>>();
1980 for (path, binding) in &mut bindings {
1981 if resolved_callers.contains(path) {
1982 binding.resolved_dependencies.clear();
1983 binding.surface_queries.clear();
1984 binding.resolution_facts = ConfigConsultations::default();
1985 } else if let Some(old) = cached.get(path) {
1986 binding.resolved_dependencies = old.resolved_dependencies.clone();
1987 binding.surface_queries = old.surface_queries.clone();
1988 }
1989 }
1990 profile.finish("index_and_surface_replay");
1991 for caller in &resolved_callers {
1994 if extracts.contains_key(caller) {
1995 continue;
1996 }
1997 let path = RelPath::new(caller.as_bytes().to_vec()).expect("bound manifest path");
1998 let Some(ManifestEntry::Regular { planes, .. }) = manifest.get(&path) else {
1999 continue;
2000 };
2001 let key = planes.callgraph.as_ref().expect("bound caller key");
2002 let decoded_blob = decode(key)?;
2003 let CallgraphBlob::Parse(blob) = decoded_blob.as_ref() else {
2004 continue;
2005 };
2006 decoded_caller_blobs += 1;
2007 facts.take();
2008 facts.take_config();
2009 let extract =
2010 blob.bind_with_dependencies(caller, &paths, Some(&bindings[caller].references))?;
2011 let caller_work = work.entry(caller.clone()).or_default();
2012 for (raw, bound) in blob.refs.iter().zip(&extract.raw_refs) {
2013 caller_work.push((
2014 CallerRefKey {
2015 caller_blob_key: key.clone(),
2016 ref_ordinal: raw.ordinal,
2017 caller_path: caller.as_bytes().to_vec(),
2018 },
2019 (raw.kind, bound.clone()),
2020 ));
2021 }
2022 extracts.insert(caller.clone(), extract);
2023 }
2024 let index = ManifestProjectIndex::from_parts(
2025 root,
2026 index.files,
2027 extracts
2028 .iter()
2029 .map(|(path, extract)| (path.clone(), &extract.data))
2030 .collect(),
2031 super::WorkspaceCratePrefixCache::default(),
2032 facts.clone(),
2033 );
2034 profile.finish("decode_resolved_callers");
2035 let surface_index = ViewSurfaceIndex {
2036 inner: &index,
2037 queries: Default::default(),
2038 };
2039 let bases: BTreeMap<_, BTreeSet<_>> = resolved_callers
2040 .iter()
2041 .map(|caller| {
2042 let binding = &bindings[caller];
2043 (
2044 caller.clone(),
2045 binding
2046 .references
2047 .values()
2048 .flatten()
2049 .chain(binding.binding_probes.iter())
2050 .cloned()
2051 .collect(),
2052 )
2053 })
2054 .collect();
2055 let mut resolved_bindings = 0;
2056 for (caller, mut caller_work) in work {
2057 if !resolved_callers.contains(&caller) {
2058 continue;
2059 }
2060 caller_work.sort_by(|a, b| (&a.0, a.1 .0).cmp(&(&b.0, b.1 .0)));
2061 let caller_data = &extracts[&caller].data;
2062 let basis = &bases[&caller];
2063 let binding = bindings
2064 .get_mut(&caller)
2065 .expect("bound caller dependencies");
2066 let mut resolutions =
2067 HashMap::<ViewResolutionBinding, (Option<String>, Option<String>)>::new();
2068 let mut caller_queries = BTreeMap::new();
2069 for (key, (kind, raw)) in caller_work {
2070 let memo_key = ViewResolutionBinding::new(&raw, caller_data);
2071 binding.resolved_dependencies.extend(
2074 raw.dependencies
2075 .iter()
2076 .filter(|dependency| !basis.contains(*dependency))
2077 .cloned(),
2078 );
2079 let (target_file, target_symbol) = match resolutions.entry(memo_key) {
2080 std::collections::hash_map::Entry::Occupied(entry) => entry.get().clone(),
2081 std::collections::hash_map::Entry::Vacant(entry) => {
2082 facts.take();
2083 facts.take_config();
2084 let resolved = super::resolve_ref(raw, &surface_index)
2085 .map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
2086 binding.resolution_facts.extend(&facts.take_config());
2089 caller_queries.extend(surface_index.take());
2090 binding.resolved_dependencies.extend(
2091 resolved
2092 .dependencies
2093 .into_iter()
2094 .chain(facts.take())
2095 .filter(|dependency| !basis.contains(dependency)),
2096 );
2097 entry
2098 .insert((resolved.target_file, resolved.target_symbol))
2099 .clone()
2100 }
2101 };
2102 result.rows.insert(DerivedRow {
2103 caller_blob_key: key.caller_blob_key.clone(),
2104 ref_ordinal: key.ref_ordinal,
2105 caller_path: key.caller_path.clone(),
2106 kind,
2107 status: if target_file.is_some() {
2108 ResolutionStatus::Resolved
2109 } else {
2110 ResolutionStatus::Unresolved
2111 },
2112 target_path: target_file.map(String::into_bytes),
2113 target_symbol,
2114 });
2115 result.resolution_order.push(key);
2116 }
2117 resolved_bindings += resolutions.len();
2118 binding.surface_queries = caller_queries.into_iter().collect();
2119 }
2120 result.resolution_order.sort();
2121 profile.finish("resolve_and_record");
2122 for binding in bindings.values_mut() {
2123 binding.consulted_facts = binding
2124 .binding_facts
2125 .facts
2126 .union(&binding.resolution_facts.facts)
2127 .cloned()
2128 .collect();
2129 binding.unattributed =
2130 binding.binding_facts.unattributed() || binding.resolution_facts.unattributed();
2131 binding.dependencies = binding
2132 .references
2133 .values()
2134 .flatten()
2135 .cloned()
2136 .chain(binding.binding_probes.iter().cloned())
2137 .chain(binding.resolved_dependencies.iter().cloned())
2138 .chain(
2139 binding
2140 .surface_queries
2141 .iter()
2142 .flat_map(|(query, _)| query.dependencies()),
2143 )
2144 .collect();
2145 }
2146 profile.finish("dependency_union");
2147 if std::env::var_os("AFT_VIEW_PROFILE").is_some() {
2148 eprintln!("view_profile join.surfaces restored={restored_surfaces} rebuilt_changed={rebuilt_changed} rebuilt_facts={rebuilt_facts} rebuilt_membership={rebuilt_membership} rebuilt_missing={rebuilt_missing} selected={} resolved={} skipped={}", selected.map_or(bindings.len(), BTreeSet::len), resolved_callers.len(), bindings.keys().filter(|path| selected.is_none_or(|set| set.contains(*path)) && !resolved_callers.contains(*path)).count());
2149 }
2150 if let Some(error) = read_error.borrow_mut().take() {
2151 return Err(error);
2152 }
2153 Ok(SelectedManifestJoin {
2154 result,
2155 bindings,
2156 resolved_callers,
2157 rebuilt_surface_entries: rebuilt_surface_paths.len(),
2158 rebuilt_surface_paths,
2159 decoded_caller_blobs,
2160 resolved_bindings,
2161 })
2162}
2163
2164#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
2169enum ViewSurfaceQuery {
2170 Language(String),
2171 Module(String, String),
2172 Parent(String),
2173 Reexports(String),
2174 Node(String, String),
2175 Callable(String, String),
2176 Alias(String, String),
2177 Export(String, String),
2178 Default(String),
2179 Contains(String),
2180 Crate(String),
2181 CrateRoot(String),
2182 Inline(String, Vec<String>, String),
2183}
2184
2185fn surface_value(value: &impl Serialize) -> String {
2186 blake3::hash(&serde_json::to_vec(value).expect("resolver surface serializes"))
2187 .to_hex()
2188 .to_string()
2189}
2190
2191fn reexport_surface(value: &[super::ReexportIndex]) -> String {
2192 surface_value(
2193 &value
2194 .iter()
2195 .map(|entry| {
2196 (
2197 &entry.target_file,
2198 entry.named.iter().collect::<BTreeMap<_, _>>(),
2199 entry.wildcard,
2200 )
2201 })
2202 .collect::<Vec<_>>(),
2203 )
2204}
2205
2206pub(crate) const VIEW_CONFIG_MEMBERSHIP_DOMAIN: &str = "\0view:config-membership";
2207pub(crate) const VIEW_RUST_MODULE_DOMAIN: &str = "\0view:rust-module-index";
2208
2209impl ViewSurfaceQuery {
2210 fn dependencies(&self) -> Vec<String> {
2211 match self {
2212 Self::Crate(_) => vec![VIEW_CONFIG_MEMBERSHIP_DOMAIN.into()],
2213 Self::CrateRoot(file) => {
2214 vec![file.clone(), VIEW_CONFIG_MEMBERSHIP_DOMAIN.into()]
2215 }
2216 Self::Parent(file) | Self::Inline(file, ..) => {
2217 vec![file.clone(), VIEW_RUST_MODULE_DOMAIN.into()]
2218 }
2219 Self::Language(file)
2220 | Self::Module(file, _)
2221 | Self::Reexports(file)
2222 | Self::Node(file, _)
2223 | Self::Callable(file, _)
2224 | Self::Alias(file, _)
2225 | Self::Export(file, _)
2226 | Self::Default(file)
2227 | Self::Contains(file) => vec![file.clone()],
2228 }
2229 }
2230
2231 fn answer(&self, index: &impl super::ResolverIndex) -> String {
2232 match self {
2233 Self::Language(file) => {
2234 surface_value(&index.lang_for(file).map(|lang| format!("{lang:?}")))
2235 }
2236 Self::Module(file, module) => surface_value(&index.module_target(file, module)),
2237 Self::Parent(file) => surface_value(&index.module_parent(file)),
2238 Self::Reexports(file) => reexport_surface(&index.reexports_for(file)),
2239 Self::Node(file, symbol) => surface_value(&index.node_for_symbol(file, symbol)),
2240 Self::Callable(file, node) => surface_value(&index.node_is_callable(file, node)),
2241 Self::Alias(file, symbol) => surface_value(&index.export_alias(file, symbol)),
2242 Self::Export(file, symbol) => surface_value(&index.has_export(file, symbol)),
2243 Self::Default(file) => surface_value(&index.default_export(file)),
2244 Self::Contains(file) => surface_value(&index.contains_file(file)),
2245 Self::Crate(name) => surface_value(&index.crate_src_prefix(name)),
2246 Self::CrateRoot(file) => surface_value(&index.rust_crate_root_file(file)),
2247 Self::Inline(file, segments, symbol) => {
2248 surface_value(&index.inline_scoped_target(file, segments, symbol))
2249 }
2250 }
2251 }
2252}
2253
2254struct ViewSurfaceIndex<'a, I> {
2255 inner: &'a I,
2256 queries: std::cell::RefCell<BTreeMap<ViewSurfaceQuery, String>>,
2257}
2258
2259impl<I> ViewSurfaceIndex<'_, I> {
2260 fn record(&self, query: ViewSurfaceQuery, value: &impl Serialize) {
2261 self.queries
2262 .borrow_mut()
2263 .insert(query, surface_value(value));
2264 }
2265 fn take(&self) -> BTreeMap<ViewSurfaceQuery, String> {
2266 std::mem::take(&mut *self.queries.borrow_mut())
2267 }
2268}
2269
2270impl<I: super::ResolverIndex> super::ResolverIndex for ViewSurfaceIndex<'_, I> {
2271 fn caller_data(&self, file: &str) -> Option<&FileCallData> {
2272 self.inner.caller_data(file)
2275 }
2276 fn lang_for(&self, file: &str) -> Option<LangId> {
2277 let value = self.inner.lang_for(file);
2278 self.record(
2279 ViewSurfaceQuery::Language(file.into()),
2280 &value.map(|lang| format!("{lang:?}")),
2281 );
2282 value
2283 }
2284 fn module_target(&self, file: &str, module: &str) -> Option<String> {
2285 let value = self.inner.module_target(file, module);
2286 self.record(ViewSurfaceQuery::Module(file.into(), module.into()), &value);
2287 value
2288 }
2289 fn module_parent(&self, file: &str) -> Option<(String, String)> {
2290 let value = self.inner.module_parent(file);
2291 self.record(ViewSurfaceQuery::Parent(file.into()), &value);
2292 value
2293 }
2294 fn reexports_for(&self, file: &str) -> Vec<super::ReexportIndex> {
2295 let value = self.inner.reexports_for(file);
2296 self.queries.borrow_mut().insert(
2297 ViewSurfaceQuery::Reexports(file.into()),
2298 reexport_surface(&value),
2299 );
2300 value
2301 }
2302 fn node_for_symbol(&self, file: &str, symbol: &str) -> Option<String> {
2303 let value = self.inner.node_for_symbol(file, symbol);
2304 self.record(ViewSurfaceQuery::Node(file.into(), symbol.into()), &value);
2305 value
2306 }
2307 fn node_is_callable(&self, file: &str, node: &str) -> bool {
2308 let value = self.inner.node_is_callable(file, node);
2309 self.record(ViewSurfaceQuery::Callable(file.into(), node.into()), &value);
2310 value
2311 }
2312 fn export_alias(&self, file: &str, symbol: &str) -> Option<String> {
2313 let value = self.inner.export_alias(file, symbol);
2314 self.record(ViewSurfaceQuery::Alias(file.into(), symbol.into()), &value);
2315 value
2316 }
2317 fn has_export(&self, file: &str, symbol: &str) -> bool {
2318 let value = self.inner.has_export(file, symbol);
2319 self.record(ViewSurfaceQuery::Export(file.into(), symbol.into()), &value);
2320 value
2321 }
2322 fn default_export(&self, file: &str) -> Option<String> {
2323 let value = self.inner.default_export(file);
2324 self.record(ViewSurfaceQuery::Default(file.into()), &value);
2325 value
2326 }
2327 fn contains_file(&self, file: &str) -> bool {
2328 let value = self.inner.contains_file(file);
2329 self.record(ViewSurfaceQuery::Contains(file.into()), &value);
2330 value
2331 }
2332 fn crate_src_prefix(&self, name: &str) -> Option<String> {
2333 let value = self.inner.crate_src_prefix(name);
2334 self.record(ViewSurfaceQuery::Crate(name.into()), &value);
2335 value
2336 }
2337 fn rust_crate_root_file(&self, file: &str) -> Option<String> {
2338 let value = self.inner.rust_crate_root_file(file);
2339 self.record(ViewSurfaceQuery::CrateRoot(file.into()), &value);
2340 value
2341 }
2342 fn inline_scoped_target(
2343 &self,
2344 file: &str,
2345 segments: &[String],
2346 symbol: &str,
2347 ) -> Option<(String, String)> {
2348 let value = self.inner.inline_scoped_target(file, segments, symbol);
2349 self.record(
2350 ViewSurfaceQuery::Inline(file.into(), segments.to_vec(), symbol.into()),
2351 &value,
2352 );
2353 value
2354 }
2355}
2356
2357#[cfg(test)]
2358mod consultation_tests {
2359 use super::*;
2360
2361 #[test]
2362 fn opaque_config_read_is_unattributed_even_after_a_known_field_read() {
2363 let manifest = Manifest::new([(
2364 RelPath::new(b"package.json".to_vec()).unwrap(),
2365 ManifestEntry::Regular {
2366 mode: 0o100644,
2367 planes: crate::views::RegularPlanes {
2368 callgraph: Some("config".into()),
2369 semantic: None,
2370 },
2371 resolution_input: true,
2372 },
2373 )])
2374 .unwrap();
2375 let bytes: Arc<[u8]> = CallgraphBlob::config(b"{}".to_vec(), "fixture")
2376 .to_bytes()
2377 .unwrap()
2378 .into();
2379 let reader = |_: &BlobKey| Some(bytes.clone());
2380 let facts = ViewBindingFacts::new(Rc::new(ManifestFacts {
2381 manifest: &manifest,
2382 blobs: &reader,
2383 }));
2384 facts.config_fact(b"package.json", "name");
2385 assert!(facts.attributed_config_bytes(b"package.json").is_some());
2386 assert!(!facts.consultations.borrow().unattributed());
2387 facts.memo_start(Path::new("/"), "test", "");
2388 assert!(facts.config_bytes(b"package.json").is_some());
2389 facts.memo_finish(Path::new("/"), "test", "");
2390 assert!(facts.take_config().unattributed());
2391 facts.memo_replay(Path::new("/"), "test", "");
2392 assert!(facts.take_config().unattributed());
2393 }
2394}
2395
2396#[cfg(test)]
2397thread_local! {
2398 static CALLER_NODE_LOOKUP_WORK: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
2399 pub(crate) static SCAN_CALLER_NODES: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
2402}
2403
2404#[test]
2405fn binding_caller_node_lookup_is_linear_and_preserves_first_duplicate() {
2406 let source = (0..200)
2407 .map(|i| format!("export function f{i}() {{ f0(); }}\n"))
2408 .collect::<String>();
2409 let blob = CallgraphBlob::extract(&source, "typescript", "lookup-test").unwrap();
2410 let mut parse = blob.parse().unwrap().clone();
2411 let mut duplicate = parse.symbols[0].clone();
2412 duplicate.ordinal += 10000;
2413 parse.symbols.push(duplicate);
2414 let manifest = Manifest::new([]).unwrap();
2415 let reader = |_: &BlobKey| None;
2416 let facts = ManifestFacts {
2417 manifest: &manifest,
2418 blobs: &reader,
2419 };
2420 let paths = FactPaths {
2421 root: Path::new("/"),
2422 facts: &facts,
2423 };
2424 CALLER_NODE_LOOKUP_WORK.with(|work| work.set(0));
2425 let extract = parse
2426 .bind_with_dependencies("caller.ts", &paths, None)
2427 .unwrap();
2428 let work = CALLER_NODE_LOOKUP_WORK.with(std::cell::Cell::get);
2429 assert!(work > 0, "lookup work must actually be observed");
2430 assert!(
2431 work <= parse.symbols.len() + parse.refs.len(),
2432 "caller node lookup must be linear: {work} operations for {} symbols and {} refs",
2433 parse.symbols.len(),
2434 parse.refs.len()
2435 );
2436 for reference in &extract.raw_refs {
2437 let expected = reference.caller_symbol.as_ref().and_then(|caller| {
2438 extract
2439 .nodes
2440 .iter()
2441 .find(|node| &node.scoped_name == caller)
2442 .map(|node| node.id.clone())
2443 });
2444 assert_eq!(reference.caller_node, expected);
2445 }
2446}