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