use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use serde::{Deserialize, Serialize};
use tree_sitter::Parser;
use super::facts::{BlobKey, FactPaths, ManifestFacts, ProjectFacts};
use crate::callgraph::{self, FileCallData, SymbolMeta};
use crate::imports::{ImportBlock, ImportForm, ImportGroup, ImportKind, ImportStatement};
use crate::parser::{grammar_for, LangId};
use crate::symbols::SymbolKind;
use crate::views::{Manifest, ManifestEntry, RelPath};
use std::collections::HashMap;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
const TOP_LEVEL_SYMBOL: &str = "<top-level>";
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ManifestJoinError {
UnsupportedLanguage(String),
Parse(String),
InvalidBlob(String),
MissingBlob(String),
InvalidConfig { path: Vec<u8>, reason: String },
}
impl fmt::Display for ManifestJoinError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedLanguage(language) => {
write!(formatter, "unsupported callgraph blob language {language}")
}
Self::Parse(reason) => write!(formatter, "callgraph blob parse failed: {reason}"),
Self::InvalidBlob(reason) => write!(formatter, "invalid callgraph blob: {reason}"),
Self::MissingBlob(key) => write!(
formatter,
"manifest references missing callgraph blob {key}"
),
Self::InvalidConfig { path, reason } => write!(
formatter,
"invalid manifest config {}: {reason}",
String::from_utf8_lossy(path)
),
}
}
}
impl std::error::Error for ManifestJoinError {}
pub trait ManifestBlobReader {
fn read_callgraph_blob(&self, full_key: &str) -> Result<Option<Vec<u8>>, ManifestJoinError>;
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct AstPreorderNode {
pub ordinal: u32,
pub kind: String,
pub byte_start: usize,
pub byte_end: usize,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct BlobSymbol {
pub ordinal: u32,
pub name: String,
pub scoped_name: String,
pub kind: String,
pub exported: bool,
pub is_default_export: bool,
pub start_line: u32,
pub start_col: u32,
pub end_line: u32,
pub end_col: u32,
pub signature: Option<String>,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BlobRefKind {
Call,
ValueRef,
Import,
Module,
Reexport,
ExportAlias,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct BlobRef {
pub ordinal: u32,
pub kind: BlobRefKind,
pub caller_symbol: Option<String>,
pub short_name: Option<String>,
pub full_ref: Option<String>,
pub module_path: Option<String>,
pub line: u32,
pub byte_start: usize,
pub byte_end: usize,
pub path_override: Option<String>,
pub local_name: Option<String>,
pub requested_name: Option<String>,
pub namespace_alias: Option<String>,
pub wildcard: bool,
pub import_kind: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct BlobImport {
pub ordinal: u32,
pub module_path: String,
pub names: Vec<String>,
pub default_import: Option<String>,
pub namespace_import: Option<String>,
pub byte_start: usize,
pub byte_end: usize,
pub raw_text: String,
pub type_only: bool,
pub side_effect: bool,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ParseBlob {
pub extractor_version: String,
pub language: String,
pub ast_nodes: Vec<AstPreorderNode>,
pub symbols: Vec<BlobSymbol>,
pub default_export_symbol: Option<String>,
pub exported_symbols: Vec<String>,
pub callable_symbols: Vec<String>,
pub imports: Vec<BlobImport>,
pub refs: Vec<BlobRef>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct ConfigBlob {
pub extractor_version: String,
pub language: String,
pub source: Vec<u8>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CallgraphBlob {
Parse(ParseBlob),
Config(ConfigBlob),
}
impl CallgraphBlob {
pub fn extract(
source: &str,
language: &str,
extractor_version: impl Into<String>,
) -> Result<Self, ManifestJoinError> {
let lang = language_id(language)
.ok_or_else(|| ManifestJoinError::UnsupportedLanguage(language.to_string()))?;
let extractor_version = extractor_version.into();
let ast_nodes = ast_preorder_nodes(source, lang)?;
let mut data = callgraph::build_file_data_from_source_with_lang(
std::path::Path::new("__callgraph_blob__"),
source,
lang,
)
.map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
if lang == LangId::Rust {
super::extend_rust_imports_with_nested_uses(source, &mut data);
}
let symbols = blob_symbols(source, &data, &ast_nodes);
let imports = blob_imports(&data, &ast_nodes);
let mut refs = blob_refs(source, &data, &ast_nodes);
refs.extend(rust_module_refs(source, lang, &ast_nodes));
let empty =
Manifest::new([]).map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
let reader = |_: &BlobKey| None;
let empty_facts = ManifestFacts {
manifest: &empty,
blobs: &reader,
};
let paths = FactPaths {
root: Path::new("/"),
facts: &empty_facts,
};
let file = Path::new("/__callgraph_blob__");
let mut structural =
super::collect_reexport_refs(paths.root, file, "__callgraph_blob__", source, &paths)
.raw_refs;
structural.extend(
super::collect_source_less_export_alias_refs("__callgraph_blob__", source).raw_refs,
);
if lang == LangId::Rust {
structural.extend(
super::collect_rust_pub_use_reexport_refs(
paths.root,
file,
"__callgraph_blob__",
&data.import_block.imports,
&super::LineIndex::new(source),
&paths,
)
.raw_refs,
);
}
refs.extend(
structural
.into_iter()
.map(|raw| structural_ref(raw, &ast_nodes)),
);
let mut exported_symbols = data.exported_symbols.clone();
exported_symbols.sort();
let mut callable_symbols = data.calls_by_symbol.keys().cloned().collect::<Vec<_>>();
callable_symbols.sort();
refs.sort_by(|left, right| {
(
left.ordinal,
left.kind,
left.byte_start,
left.byte_end,
left.full_ref.as_deref(),
)
.cmp(&(
right.ordinal,
right.kind,
right.byte_start,
right.byte_end,
right.full_ref.as_deref(),
))
});
refs.dedup_by(|left, right| {
left.ordinal == right.ordinal
&& left.kind == right.kind
&& left.byte_start == right.byte_start
&& left.byte_end == right.byte_end
&& left.full_ref == right.full_ref
});
Ok(Self::Parse(ParseBlob {
extractor_version,
language: language.to_string(),
ast_nodes,
symbols,
default_export_symbol: data.default_export_symbol,
exported_symbols,
callable_symbols,
imports,
refs,
}))
}
pub fn config(source: impl Into<Vec<u8>>, extractor_version: impl Into<String>) -> Self {
Self::Config(ConfigBlob {
extractor_version: extractor_version.into(),
language: "config".to_string(),
source: source.into(),
})
}
pub fn to_bytes(&self) -> Result<Vec<u8>, ManifestJoinError> {
serde_json::to_vec(self).map_err(|error| ManifestJoinError::InvalidBlob(error.to_string()))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ManifestJoinError> {
serde_json::from_slice(bytes)
.map_err(|error| ManifestJoinError::InvalidBlob(error.to_string()))
}
pub fn parse(&self) -> Option<&ParseBlob> {
match self {
Self::Parse(blob) => Some(blob),
Self::Config(_) => None,
}
}
pub fn config_source(&self) -> Option<&ConfigBlob> {
match self {
Self::Parse(_) => None,
Self::Config(blob) => Some(blob),
}
}
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct CallerRefKey {
pub caller_blob_key: String,
pub ref_ordinal: u32,
pub caller_path: Vec<u8>,
}
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum ResolutionStatus {
Resolved,
Unresolved,
}
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct DerivedRow {
pub caller_blob_key: String,
pub ref_ordinal: u32,
pub caller_path: Vec<u8>,
pub kind: BlobRefKind,
pub status: ResolutionStatus,
pub target_path: Option<Vec<u8>>,
pub target_symbol: Option<String>,
}
impl DerivedRow {
pub fn ref_key(&self) -> CallerRefKey {
CallerRefKey {
caller_blob_key: self.caller_blob_key.clone(),
ref_ordinal: self.ref_ordinal,
caller_path: self.caller_path.clone(),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct JoinResult {
pub rows: BTreeSet<DerivedRow>,
pub resolution_order: Vec<CallerRefKey>,
pub unbound_non_utf8_paths: Vec<Vec<u8>>,
}
impl JoinResult {
pub fn canonical_serialization(&self) -> Vec<u8> {
let mut output = Vec::new();
for row in &self.rows {
append_field(&mut output, row.caller_blob_key.as_bytes());
append_field(&mut output, &row.ref_ordinal.to_be_bytes());
append_field(&mut output, &row.caller_path);
append_field(&mut output, &[row.kind as u8]);
append_field(
&mut output,
&[match row.status {
ResolutionStatus::Resolved => 1,
ResolutionStatus::Unresolved => 0,
}],
);
append_optional_field(&mut output, row.target_path.as_deref());
append_optional_field(&mut output, row.target_symbol.as_deref().map(str::as_bytes));
}
output
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct IncrementalJoinResult {
pub result: JoinResult,
pub re_resolved: BTreeSet<CallerRefKey>,
pub full_re_resolve: bool,
}
fn changed_manifest_paths(previous: &Manifest, current: &Manifest) -> BTreeSet<Vec<u8>> {
let previous_entries = previous
.entries()
.map(|(path, entry)| (path.as_bytes().to_vec(), entry))
.collect::<BTreeMap<_, _>>();
let current_entries = current
.entries()
.map(|(path, entry)| (path.as_bytes().to_vec(), entry))
.collect::<BTreeMap<_, _>>();
previous_entries
.keys()
.chain(current_entries.keys())
.collect::<BTreeSet<_>>()
.into_iter()
.filter(|path| previous_entries.get(*path) != current_entries.get(*path))
.cloned()
.collect()
}
fn manifest_resolution_input(manifest: &Manifest, path: &[u8]) -> bool {
let lookup = if path.first() == Some(&0) {
return manifest
.entries()
.find(|(candidate, _)| candidate.as_bytes() == path)
.is_some_and(|(_, entry)| matches!(entry, ManifestEntry::Synthetic { .. }));
} else {
RelPath::new(path.to_vec()).ok()
};
lookup
.as_ref()
.and_then(|path| manifest.get(path))
.is_some_and(|entry| {
matches!(
entry,
ManifestEntry::Regular {
resolution_input: true,
..
}
)
})
}
fn append_field(output: &mut Vec<u8>, value: &[u8]) {
output.extend_from_slice(&(value.len() as u64).to_be_bytes());
output.extend_from_slice(value);
}
fn append_optional_field(output: &mut Vec<u8>, value: Option<&[u8]>) {
match value {
Some(value) => {
output.push(1);
append_field(output, value);
}
None => output.push(0),
}
}
fn language_id(language: &str) -> Option<LangId> {
Some(match language {
"typescript" => LangId::TypeScript,
"tsx" => LangId::Tsx,
"javascript" => LangId::JavaScript,
"python" => LangId::Python,
"rust" => LangId::Rust,
"go" => LangId::Go,
"c" => LangId::C,
"cpp" => LangId::Cpp,
"cuda" => LangId::Cuda,
"metal" => LangId::Metal,
"zig" => LangId::Zig,
"csharp" => LangId::CSharp,
"bash" => LangId::Bash,
"html" => LangId::Html,
"markdown" => LangId::Markdown,
"solidity" => LangId::Solidity,
"scss" => LangId::Scss,
"vue" => LangId::Vue,
"json" => LangId::Json,
"scala" => LangId::Scala,
"java" => LangId::Java,
"ruby" => LangId::Ruby,
"kotlin" => LangId::Kotlin,
"swift" => LangId::Swift,
"php" => LangId::Php,
"lua" => LangId::Lua,
"perl" => LangId::Perl,
"yaml" => LangId::Yaml,
"pascal" => LangId::Pascal,
"r" => LangId::R,
"groovy" => LangId::Groovy,
"objc" => LangId::ObjC,
"toml" => LangId::Toml,
_ => return None,
})
}
fn ast_preorder_nodes(
source: &str,
lang: LangId,
) -> Result<Vec<AstPreorderNode>, ManifestJoinError> {
let mut parser = Parser::new();
parser
.set_language(&grammar_for(lang))
.map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
let tree = parser
.parse(source, None)
.ok_or_else(|| ManifestJoinError::Parse("tree-sitter returned no tree".to_string()))?;
let mut nodes = Vec::new();
let mut stack = vec![tree.root_node()];
while let Some(node) = stack.pop() {
nodes.push(AstPreorderNode {
ordinal: nodes.len() as u32,
kind: node.kind().to_string(),
byte_start: node.start_byte(),
byte_end: node.end_byte(),
});
let children = node.children(&mut node.walk()).collect::<Vec<_>>();
stack.extend(children.into_iter().rev());
}
Ok(nodes)
}
fn blob_symbols(
source: &str,
data: &FileCallData,
ast_nodes: &[AstPreorderNode],
) -> Vec<BlobSymbol> {
let mut symbols = data
.symbol_metadata
.iter()
.map(|(scoped_name, meta)| {
blob_symbol(
source,
scoped_name,
meta,
&data.default_export_symbol,
ast_nodes,
)
})
.collect::<Vec<_>>();
symbols.sort_by(|left, right| {
(
left.ordinal,
left.start_line,
left.start_col,
left.scoped_name.as_str(),
)
.cmp(&(
right.ordinal,
right.start_line,
right.start_col,
right.scoped_name.as_str(),
))
});
symbols
}
fn blob_symbol(
source: &str,
scoped_name: &str,
meta: &SymbolMeta,
default_export: &Option<String>,
ast_nodes: &[AstPreorderNode],
) -> BlobSymbol {
let byte_start = byte_offset(source, meta.range.start_line, meta.range.start_col);
let byte_end = byte_offset(source, meta.range.end_line, meta.range.end_col).max(byte_start);
BlobSymbol {
ordinal: ordinal_for_range(ast_nodes, byte_start, byte_end),
name: unqualified_symbol_name(scoped_name).to_string(),
scoped_name: scoped_name.to_string(),
kind: symbol_kind_name(&meta.kind).to_string(),
exported: meta.exported,
is_default_export: default_export.as_deref() == Some(scoped_name),
start_line: meta.range.start_line,
start_col: meta.range.start_col,
end_line: meta.range.end_line,
end_col: meta.range.end_col,
signature: meta.signature.clone(),
}
}
fn blob_imports(data: &FileCallData, ast_nodes: &[AstPreorderNode]) -> Vec<BlobImport> {
let mut imports = data
.import_block
.imports
.iter()
.map(|import| BlobImport {
ordinal: ordinal_for_range(ast_nodes, import.byte_range.start, import.byte_range.end),
module_path: import.module_path.clone(),
names: import.names.clone(),
default_import: import.default_import.clone(),
namespace_import: import.namespace_import.clone(),
byte_start: import.byte_range.start,
byte_end: import.byte_range.end,
raw_text: import.raw_text.clone(),
type_only: import.kind == ImportKind::Type,
side_effect: import.kind == ImportKind::SideEffect,
})
.collect::<Vec<_>>();
imports.sort_by(|left, right| {
(left.ordinal, left.module_path.as_str()).cmp(&(right.ordinal, right.module_path.as_str()))
});
imports
}
fn blob_refs(source: &str, data: &FileCallData, ast_nodes: &[AstPreorderNode]) -> Vec<BlobRef> {
let mut refs = Vec::new();
for (caller_symbol, calls) in &data.calls_by_symbol {
for call in calls {
refs.push(call_ref(caller_symbol, call, BlobRefKind::Call, ast_nodes));
}
}
for (caller_symbol, calls) in &data.value_refs_by_symbol {
for call in calls {
refs.push(call_ref(
caller_symbol,
call,
BlobRefKind::ValueRef,
ast_nodes,
));
}
}
for import in &data.import_block.imports {
refs.push(BlobRef {
ordinal: ordinal_for_range(ast_nodes, import.byte_range.start, import.byte_range.end),
kind: BlobRefKind::Import,
caller_symbol: None,
short_name: None,
full_ref: Some(import.module_path.clone()),
module_path: Some(import.module_path.clone()),
line: line_for_byte(source, import.byte_range.start),
byte_start: import.byte_range.start,
byte_end: import.byte_range.end,
path_override: None,
local_name: None,
requested_name: None,
namespace_alias: import.namespace_import.clone(),
wildcard: super::import_is_wildcard(import),
import_kind: None,
});
}
refs
}
fn call_ref(
caller_symbol: &str,
call: &callgraph::CallSite,
kind: BlobRefKind,
ast_nodes: &[AstPreorderNode],
) -> BlobRef {
BlobRef {
ordinal: ordinal_for_range(ast_nodes, call.byte_start, call.byte_end),
kind,
caller_symbol: Some(caller_symbol.to_string()),
short_name: Some(call.callee_name.clone()),
full_ref: Some(call.full_callee.clone()),
module_path: None,
line: call.line,
byte_start: call.byte_start,
byte_end: call.byte_end,
path_override: None,
local_name: None,
requested_name: None,
namespace_alias: None,
wildcard: false,
import_kind: None,
}
}
fn rust_module_refs(source: &str, lang: LangId, ast_nodes: &[AstPreorderNode]) -> Vec<BlobRef> {
if lang != LangId::Rust {
return Vec::new();
}
let mut parser = Parser::new();
if parser.set_language(&grammar_for(lang)).is_err() {
return Vec::new();
}
let Some(tree) = parser.parse(source, None) else {
return Vec::new();
};
let mut refs = Vec::new();
let mut stack = vec![tree.root_node()];
while let Some(node) = stack.pop() {
if node.kind() == "mod_item"
&& node
.named_children(&mut node.walk())
.all(|child| child.kind() != "declaration_list")
{
if let Some(name) = node.child_by_field_name("name") {
let module_name = source[name.byte_range()].to_string();
refs.push(BlobRef {
ordinal: ordinal_for_range(ast_nodes, node.start_byte(), node.end_byte()),
kind: BlobRefKind::Module,
caller_symbol: None,
short_name: Some(module_name.clone()),
full_ref: Some(module_name.clone()),
module_path: Some(module_name),
line: node.start_position().row as u32 + 1,
byte_start: node.start_byte(),
byte_end: node.end_byte(),
path_override: super::rust_module_path_override(source, node)
.map(str::to_string),
local_name: None,
requested_name: None,
namespace_alias: None,
wildcard: false,
import_kind: None,
});
}
}
let children = node.children(&mut node.walk()).collect::<Vec<_>>();
stack.extend(children.into_iter().rev());
}
refs
}
fn ordinal_for_range(ast_nodes: &[AstPreorderNode], byte_start: usize, byte_end: usize) -> u32 {
ast_nodes
.iter()
.filter(|node| node.byte_start <= byte_start && node.byte_end >= byte_end)
.min_by_key(|node| (node.byte_end.saturating_sub(node.byte_start), node.ordinal))
.map(|node| node.ordinal)
.unwrap_or(0)
}
fn byte_offset(source: &str, line: u32, column: u32) -> usize {
let mut offset = 0usize;
for (index, segment) in source.split_inclusive('\n').enumerate() {
if index as u32 == line {
return offset + (column as usize).min(segment.len());
}
offset += segment.len();
}
source.len()
}
fn line_for_byte(source: &str, byte_start: usize) -> u32 {
source[..byte_start.min(source.len())]
.bytes()
.filter(|byte| *byte == b'\n')
.count() as u32
+ 1
}
fn symbol_kind_name(kind: &SymbolKind) -> &'static str {
match kind {
SymbolKind::Function => "function",
SymbolKind::Kernel => "kernel",
SymbolKind::Class => "class",
SymbolKind::Method => "method",
SymbolKind::Struct => "struct",
SymbolKind::Interface => "interface",
SymbolKind::Enum => "enum",
SymbolKind::TypeAlias => "type_alias",
SymbolKind::Variable => "variable",
SymbolKind::Heading => "heading",
SymbolKind::FileSummary => "file_summary",
}
}
fn unqualified_symbol_name(scoped_name: &str) -> &str {
if scoped_name == TOP_LEVEL_SYMBOL {
return scoped_name;
}
scoped_name.rsplit("::").next().unwrap_or(scoped_name)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blob_ordinals_are_tree_sitter_preorder_positions() {
let source = "export function run() { return helper(); }\nfunction helper() {}\n";
let blob = CallgraphBlob::extract(source, "typescript", "join-test-v1").unwrap();
let repeated = CallgraphBlob::extract(source, "typescript", "join-test-v1").unwrap();
let different_version =
CallgraphBlob::extract(source, "typescript", "join-test-v2").unwrap();
assert_eq!(blob.to_bytes().unwrap(), repeated.to_bytes().unwrap());
assert_ne!(
blob.to_bytes().unwrap(),
different_version.to_bytes().unwrap()
);
let parse = blob.parse().unwrap();
assert_eq!(parse.ast_nodes[0].ordinal, 0);
assert!(parse.refs.iter().all(|reference| parse
.ast_nodes
.iter()
.any(|node| node.ordinal == reference.ordinal)));
let helper = parse
.refs
.iter()
.find(|reference| reference.short_name.as_deref() == Some("helper"))
.unwrap();
let node = parse
.ast_nodes
.iter()
.find(|node| node.ordinal == helper.ordinal)
.unwrap();
assert!(node.byte_start <= helper.byte_start && node.byte_end >= helper.byte_end);
assert_eq!(node.kind, "call_expression");
let mut parser = Parser::new();
parser
.set_language(&grammar_for(LangId::TypeScript))
.unwrap();
let tree = parser.parse(source, None).unwrap();
let mut cursor = tree.walk();
let mut expected = Vec::new();
'preorder: loop {
let node = cursor.node();
expected.push(AstPreorderNode {
ordinal: expected.len() as u32,
kind: node.kind().to_string(),
byte_start: node.start_byte(),
byte_end: node.end_byte(),
});
if cursor.goto_first_child() {
continue;
}
while !cursor.goto_next_sibling() {
if !cursor.goto_parent() {
break 'preorder;
}
}
}
assert_eq!(parse.ast_nodes, expected);
}
}
fn structural_ref(raw: super::RawRef, nodes: &[AstPreorderNode]) -> BlobRef {
BlobRef {
ordinal: ordinal_for_range(nodes, raw.byte_start, raw.byte_end),
kind: if raw.kind == "export_alias" {
BlobRefKind::ExportAlias
} else {
BlobRefKind::Reexport
},
caller_symbol: raw.caller_symbol,
short_name: raw.short_name,
full_ref: raw.full_ref,
module_path: raw.module_path,
line: raw.line,
byte_start: raw.byte_start,
byte_end: raw.byte_end,
path_override: None,
local_name: raw.local_name,
requested_name: raw.requested_name,
namespace_alias: raw.namespace_alias,
wildcard: raw.wildcard,
import_kind: raw.import_kind,
}
}
fn bound_name(name: &str, path: &str) -> String {
name.replace(
"<default:__callgraph_blob__>",
&format!(
"<default:{}>",
Path::new(path)
.file_name()
.unwrap_or_default()
.to_string_lossy()
),
)
}
impl ParseBlob {
fn file_data(&self, path: &str) -> Result<FileCallData, ManifestJoinError> {
let lang = language_id(&self.language)
.ok_or_else(|| ManifestJoinError::UnsupportedLanguage(self.language.clone()))?;
let mut calls_by_symbol: HashMap<String, Vec<callgraph::CallSite>> = HashMap::new();
let mut value_refs_by_symbol: HashMap<String, Vec<callgraph::CallSite>> = HashMap::new();
for raw in &self.refs {
let map = match raw.kind {
BlobRefKind::Call => &mut calls_by_symbol,
BlobRefKind::ValueRef => &mut value_refs_by_symbol,
_ => continue,
};
let Some(caller) = &raw.caller_symbol else {
continue;
};
map.entry(bound_name(caller, path))
.or_default()
.push(callgraph::CallSite {
callee_name: raw.short_name.clone().unwrap_or_default(),
full_callee: raw.full_ref.clone().unwrap_or_default(),
line: raw.line,
byte_start: raw.byte_start,
byte_end: raw.byte_end,
});
}
for symbol in &self.callable_symbols {
calls_by_symbol.entry(bound_name(symbol, path)).or_default();
}
let mut symbol_metadata = HashMap::new();
for symbol in &self.symbols {
let kind = match symbol.kind.as_str() {
"function" => SymbolKind::Function,
"method" => SymbolKind::Method,
"class" => SymbolKind::Class,
"struct" => SymbolKind::Struct,
"interface" => SymbolKind::Interface,
"enum" => SymbolKind::Enum,
"type_alias" => SymbolKind::TypeAlias,
"heading" => SymbolKind::Heading,
"file_summary" => SymbolKind::FileSummary,
_ => SymbolKind::Variable,
};
symbol_metadata.insert(
bound_name(&symbol.scoped_name, path),
SymbolMeta {
kind,
exported: symbol.exported,
signature: symbol.signature.clone(),
line: symbol.start_line + 1,
range: crate::symbols::Range {
start_line: symbol.start_line,
start_col: symbol.start_col,
end_line: symbol.end_line,
end_col: symbol.end_col,
},
entry_point_attribute: None,
},
);
}
let imports = self
.imports
.iter()
.map(|import| ImportStatement {
module_path: import.module_path.clone(),
names: import.names.clone(),
default_import: import.default_import.clone(),
namespace_import: import.namespace_import.clone(),
kind: if import.type_only {
ImportKind::Type
} else if import.side_effect {
ImportKind::SideEffect
} else {
ImportKind::Value
},
group: ImportGroup::Internal,
byte_range: import.byte_start..import.byte_end,
raw_text: import.raw_text.clone(),
form: if lang == LangId::Rust {
ImportForm::RustUse {
visibility: import.default_import.clone(),
named: import.names.clone(),
}
} else {
ImportForm::Es {
default_import: import.default_import.clone(),
namespace_import: import.namespace_import.clone(),
named: import.names.clone(),
type_only: import.type_only,
side_effect: import.side_effect,
attribute_clause: None,
attribute_type: None,
}
},
})
.collect::<Vec<_>>();
Ok(FileCallData {
calls_by_symbol,
value_refs_by_symbol,
symbol_metadata,
exported_symbols: self
.exported_symbols
.iter()
.map(|s| bound_name(s, path))
.collect(),
default_export_symbol: self
.default_export_symbol
.as_ref()
.map(|s| bound_name(s, path)),
import_block: ImportBlock {
imports,
byte_range: None,
},
lang,
})
}
fn bind(
&self,
path: &str,
facts: &FactPaths<'_>,
) -> Result<super::FileExtract, ManifestJoinError> {
self.bind_with_dependencies(path, facts, None)
}
fn bind_with_dependencies(
&self,
path: &str,
facts: &FactPaths<'_>,
cached: Option<&BTreeMap<u32, BTreeSet<String>>>,
) -> Result<super::FileExtract, ManifestJoinError> {
let data = self.file_data(path)?;
let nodes = self
.symbols
.iter()
.map(|symbol| {
let scoped_name = bound_name(&symbol.scoped_name, path);
super::NodeRecord {
id: format!("{path}:{}:{scoped_name}", symbol.ordinal),
file_path: path.to_string(),
name: bound_name(&symbol.name, path),
scoped_name,
kind: symbol.kind.clone(),
range: crate::symbols::Range {
start_line: symbol.start_line,
start_col: symbol.start_col,
end_line: symbol.end_line,
end_col: symbol.end_col,
},
range_ordinal: symbol.ordinal,
signature: symbol.signature.clone(),
exported: symbol.exported,
is_default_export: symbol.is_default_export,
is_type_like: false,
is_callgraph_entry_point: false,
}
})
.collect::<Vec<_>>();
let abs = facts.root.join(path);
let mut raw_refs = Vec::new();
for (position, raw) in self.refs.iter().enumerate() {
let position = u32::try_from(position).expect("reference vector fits u32");
let dependencies =
if let Some(dependencies) = cached.and_then(|cache| cache.get(&position)) {
dependencies.clone()
} else if raw.kind == BlobRefKind::Module {
super::rust_external_module_target(
&abs,
raw.path_override.as_deref(),
raw.module_path.as_deref().unwrap_or_default(),
facts,
)
.and_then(|p| facts.canonical(&p))
.map(|p| super::relative_path(facts.root, &p))
.into_iter()
.collect()
} else if let Some(module) = &raw.module_path {
super::module_dependencies(facts.root, &abs, module, facts)
} else {
BTreeSet::new()
};
let caller_symbol = raw
.caller_symbol
.as_ref()
.map(|name| bound_name(name, path));
let caller_node = caller_symbol.as_ref().and_then(|name| {
nodes
.iter()
.find(|n| &n.scoped_name == name)
.map(|n| n.id.clone())
});
raw_refs.push(super::RawRef {
ref_id: format!("{path}:{}:{:?}", raw.ordinal, raw.kind),
caller_node,
caller_symbol,
caller_file: path.to_string(),
kind: match raw.kind {
BlobRefKind::Call => "call",
BlobRefKind::ValueRef => "value_ref",
BlobRefKind::Import => "import",
BlobRefKind::Module => "module",
BlobRefKind::Reexport => "reexport",
BlobRefKind::ExportAlias => "export_alias",
}
.to_string(),
short_name: raw.short_name.clone(),
full_ref: raw.full_ref.clone(),
module_path: raw.module_path.clone(),
import_kind: raw.import_kind.clone(),
local_name: raw.local_name.clone(),
requested_name: raw.requested_name.clone(),
namespace_alias: raw.namespace_alias.clone(),
wildcard: raw.wildcard,
line: raw.line,
byte_start: raw.byte_start,
byte_end: raw.byte_end,
dependencies,
});
}
Ok(super::FileExtract {
rel_path: path.to_string(),
freshness: crate::cache_freshness::FileFreshness {
mtime: std::time::UNIX_EPOCH,
size: 0,
content_hash: crate::cache_freshness::zero_hash(),
},
lang: data.lang,
data,
nodes,
raw_refs,
dispatch_hints: Vec::new(),
surface_fingerprint: String::new(),
})
}
}
type ManifestProjectIndex<'a> = super::ProjectIndex<'a>;
impl JoinResult {
pub fn from_manifest(
manifest: &Manifest,
blobs: &impl ManifestBlobReader,
) -> Result<Self, ManifestJoinError> {
let loaded = manifest_payloads(manifest, blobs)?;
let reader = |key: &BlobKey| loaded.get(key).cloned();
let facts = Rc::new(ManifestFacts {
manifest,
blobs: &reader,
});
Self::from_facts(manifest, &loaded, Path::new("/"), facts, None)
}
fn from_facts<'a>(
manifest: &'a Manifest,
loaded: &BTreeMap<String, Arc<[u8]>>,
root: &Path,
facts: Rc<dyn ProjectFacts + 'a>,
selected: Option<&BTreeSet<CallerRefKey>>,
) -> Result<Self, ManifestJoinError> {
let paths = FactPaths {
root,
facts: facts.as_ref(),
};
let mut extracts = HashMap::new();
let mut work = Vec::new();
let mut unbound_non_utf8_paths = Vec::new();
for (path, entry) in manifest.entries() {
let ManifestEntry::Regular { planes, .. } = entry else {
continue;
};
let Some(key) = &planes.callgraph else {
continue;
};
let bytes = loaded
.get(key)
.ok_or_else(|| ManifestJoinError::MissingBlob(key.clone()))?;
let CallgraphBlob::Parse(blob) = CallgraphBlob::from_bytes(bytes)? else {
continue;
};
let Ok(rel) = std::str::from_utf8(path.as_bytes()) else {
unbound_non_utf8_paths.push(path.as_bytes().to_vec());
continue;
};
let extract = blob.bind(rel, &paths)?;
for (raw, bound) in blob.refs.iter().zip(&extract.raw_refs) {
let ref_key = CallerRefKey {
caller_blob_key: key.clone(),
ref_ordinal: raw.ordinal,
caller_path: path.as_bytes().to_vec(),
};
if selected.is_none_or(|set| set.contains(&ref_key)) {
work.push((ref_key, (raw.kind, bound.clone())));
}
}
extracts.insert(rel.to_string(), extract);
}
let files = extracts
.iter()
.map(|(path, extract)| {
(
path.clone(),
super::DbFileIndex::from_extract(root, extract, &paths),
)
})
.collect();
let caller_data = extracts
.iter()
.map(|(path, extract)| (path.clone(), &extract.data))
.collect();
let mut index = ManifestProjectIndex::from_parts(
root,
files,
caller_data,
super::WorkspaceCratePrefixCache::default(),
facts,
);
index.unbound_non_utf8_paths = unbound_non_utf8_paths;
if !index.unbound_non_utf8_paths.is_empty() {
log::warn!(
"callgraph index left {} non-UTF-8 source paths unbound",
index.unbound_non_utf8_paths.len()
);
}
let mut result = Self {
rows: BTreeSet::new(),
resolution_order: Vec::new(),
unbound_non_utf8_paths: index.unbound_non_utf8_paths.clone(),
};
work.sort_by(|a, b| (&a.0, a.1 .0).cmp(&(&b.0, b.1 .0)));
for (key, (kind, raw)) in work {
let resolved = super::resolve_ref(raw, &index)
.map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
result.rows.insert(DerivedRow {
caller_blob_key: key.caller_blob_key.clone(),
ref_ordinal: key.ref_ordinal,
caller_path: key.caller_path.clone(),
kind,
status: if resolved.target_file.is_some() {
ResolutionStatus::Resolved
} else {
ResolutionStatus::Unresolved
},
target_path: resolved.target_file.map(String::into_bytes),
target_symbol: resolved.target_symbol,
});
result.resolution_order.push(key);
}
Ok(result)
}
pub fn update(
&self,
previous_manifest: &Manifest,
manifest: &Manifest,
blobs: &impl ManifestBlobReader,
) -> Result<IncrementalJoinResult, ManifestJoinError> {
let changed = changed_manifest_paths(previous_manifest, manifest);
let full_re_resolve = changed.iter().any(|path| {
manifest_resolution_input(previous_manifest, path)
|| manifest_resolution_input(manifest, path)
});
let loaded = manifest_payloads(manifest, blobs)?;
let reader = |key: &BlobKey| loaded.get(key).cloned();
let mut current_keys = BTreeSet::new();
for (path, entry) in manifest.entries() {
if std::str::from_utf8(path.as_bytes()).is_err() {
continue;
}
let ManifestEntry::Regular { planes, .. } = entry else {
continue;
};
let Some(key) = &planes.callgraph else {
continue;
};
if let CallgraphBlob::Parse(blob) = CallgraphBlob::from_bytes(&loaded[key])? {
current_keys.extend(blob.refs.iter().map(|raw| CallerRefKey {
caller_blob_key: key.clone(),
ref_ordinal: raw.ordinal,
caller_path: path.as_bytes().to_vec(),
}));
}
}
let previous_rows = self
.rows
.iter()
.map(|row| (row.ref_key(), row))
.collect::<BTreeMap<_, _>>();
let selected = current_keys
.iter()
.filter(|key| {
full_re_resolve
|| changed.contains(&key.caller_path)
|| previous_rows.get(*key).is_none_or(|row| {
row.target_path
.as_ref()
.is_some_and(|path| changed.contains(path))
})
})
.cloned()
.collect::<BTreeSet<_>>();
let facts = Rc::new(ManifestFacts {
manifest,
blobs: &reader,
});
let mut result =
Self::from_facts(manifest, &loaded, Path::new("/"), facts, Some(&selected))?;
result.rows.extend(
self.rows
.iter()
.filter(|row| {
current_keys.contains(&row.ref_key()) && !selected.contains(&row.ref_key())
})
.cloned(),
);
result.resolution_order = result.rows.iter().map(DerivedRow::ref_key).collect();
result.resolution_order.sort();
Ok(IncrementalJoinResult {
result,
re_resolved: selected,
full_re_resolve,
})
}
}
fn manifest_payloads(
manifest: &Manifest,
blobs: &impl ManifestBlobReader,
) -> Result<BTreeMap<String, Arc<[u8]>>, ManifestJoinError> {
let mut loaded = BTreeMap::new();
for (_, entry) in manifest.entries() {
let ManifestEntry::Regular { planes, .. } = entry else {
continue;
};
let Some(key) = &planes.callgraph else {
continue;
};
if !loaded.contains_key(key) {
let bytes = blobs
.read_callgraph_blob(key)?
.ok_or_else(|| ManifestJoinError::MissingBlob(key.clone()))?;
loaded.insert(key.clone(), Arc::from(bytes));
}
}
Ok(loaded)
}
#[cfg(test)]
#[path = "../../tests/integration/join_manifest_test.rs"]
mod manifest_integration_tests;
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub(crate) struct ViewBindingDependencies {
pub references: BTreeMap<u32, BTreeSet<String>>,
pub dependencies: BTreeSet<String>,
#[serde(default)]
pub consulted_facts: BTreeSet<(String, String)>,
#[serde(default)]
pub unattributed: bool,
#[serde(default)]
binding_facts: ConfigConsultations,
#[serde(default)]
resolution_facts: ConfigConsultations,
binding_probes: BTreeSet<String>,
resolved_dependencies: BTreeSet<String>,
surface_queries: Vec<(ViewSurfaceQuery, String)>,
#[serde(default)]
surface: Option<ViewFileSurface>,
}
#[derive(Hash, PartialEq, Eq)]
struct ViewResolutionBinding {
caller: String,
kind: String,
full_ref: Option<String>,
short_name: Option<String>,
visible_rust_imports: usize,
}
impl ViewResolutionBinding {
fn new(raw: &super::RawRef, caller: &FileCallData) -> Self {
Self {
caller: raw.caller_file.clone(),
kind: raw.kind.clone(),
full_ref: raw.full_ref.clone(),
short_name: raw.short_name.clone(),
visible_rust_imports: if caller.lang == LangId::Rust {
caller
.import_block
.imports
.iter()
.filter(|import| import.byte_range.start <= raw.byte_start)
.count()
} else {
0
},
}
}
}
pub(crate) struct SelectedManifestJoin {
pub result: JoinResult,
pub bindings: BTreeMap<String, ViewBindingDependencies>,
pub resolved_callers: BTreeSet<String>,
pub rebuilt_surface_entries: usize,
pub decoded_caller_blobs: usize,
pub resolved_bindings: usize,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
struct ViewFileSurface {
language: String,
exports: BTreeSet<String>,
default_export: Option<String>,
export_aliases: BTreeMap<String, String>,
node_by_scoped: BTreeMap<String, String>,
node_by_bare: BTreeMap<String, String>,
node_kind_by_id: BTreeMap<String, String>,
module_targets: BTreeMap<String, Option<String>>,
declared_module_targets: BTreeMap<String, Option<String>>,
reexports: Vec<(Option<String>, BTreeMap<String, String>, bool)>,
}
impl ViewFileSurface {
fn capture(language: &str, index: &super::DbFileIndex) -> Self {
Self {
language: language.into(),
exports: index.exports.iter().cloned().collect(),
default_export: index.default_export.clone(),
export_aliases: index.export_aliases.clone().into_iter().collect(),
node_by_scoped: index.node_by_scoped.clone().into_iter().collect(),
node_by_bare: index.node_by_bare.clone().into_iter().collect(),
node_kind_by_id: index.node_kind_by_id.clone().into_iter().collect(),
module_targets: index.module_targets.clone().into_iter().collect(),
declared_module_targets: index.declared_module_targets.clone().into_iter().collect(),
reexports: index
.reexports
.iter()
.map(|r| {
(
r.target_file.clone(),
r.named.clone().into_iter().collect(),
r.wildcard,
)
})
.collect(),
}
}
fn restore(&self) -> super::DbFileIndex {
super::DbFileIndex {
lang: language_id(&self.language),
exports: self.exports.iter().cloned().collect(),
default_export: self.default_export.clone(),
export_aliases: self.export_aliases.clone().into_iter().collect(),
node_by_scoped: self.node_by_scoped.clone().into_iter().collect(),
node_by_bare: self.node_by_bare.clone().into_iter().collect(),
node_kind_by_id: self.node_kind_by_id.clone().into_iter().collect(),
module_targets: self.module_targets.clone().into_iter().collect(),
declared_module_targets: self.declared_module_targets.clone().into_iter().collect(),
reexports: self
.reexports
.iter()
.map(|(target_file, named, wildcard)| super::ReexportIndex {
target_file: target_file.clone(),
named: named.clone().into_iter().collect(),
wildcard: *wildcard,
})
.collect(),
}
}
}
pub(crate) fn view_resolution_config(path: &[u8]) -> bool {
matches!(
path.rsplit(|byte| *byte == b'/').next(),
Some(b"package.json" | b"tsconfig.json" | b"pnpm-workspace.yaml" | b"Cargo.toml")
)
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
struct ConfigConsultations {
facts: BTreeSet<(String, String)>,
reads: BTreeSet<String>,
unattributed: bool,
}
impl ConfigConsultations {
fn extend(&mut self, other: &Self) {
self.facts.extend(other.facts.iter().cloned());
self.reads.extend(other.reads.iter().cloned());
self.unattributed |= other.unattributed;
}
fn unattributed(&self) -> bool {
self.unattributed
|| self
.reads
.iter()
.any(|path| !self.facts.iter().any(|(input, _)| input == path))
}
}
#[derive(Clone, Default)]
struct ConsultationTrace {
config: ConfigConsultations,
probes: BTreeSet<String>,
}
type ConsultationMemoKey = (std::path::PathBuf, String, String);
struct ViewBindingFacts<'a> {
inner: Rc<dyn ProjectFacts + 'a>,
probes: std::cell::RefCell<BTreeSet<String>>,
consultations: std::cell::RefCell<ConfigConsultations>,
memo_stack: std::cell::RefCell<Vec<(ConsultationMemoKey, ConsultationTrace)>>,
memo_traces: std::cell::RefCell<BTreeMap<ConsultationMemoKey, ConsultationTrace>>,
workspace_packages:
std::cell::RefCell<BTreeMap<(std::path::PathBuf, String), Option<std::path::PathBuf>>>,
workspace_members:
std::cell::RefCell<BTreeMap<std::path::PathBuf, Arc<Vec<std::path::PathBuf>>>>,
canonical_cache: std::cell::RefCell<HashMap<Vec<u8>, Option<Vec<u8>>>>,
file_cache: std::cell::RefCell<HashMap<Vec<u8>, bool>>,
config_cache: std::cell::RefCell<HashMap<Vec<u8>, Option<Arc<[u8]>>>>,
directory_cache: std::cell::RefCell<HashMap<Vec<u8>, Vec<super::facts::DirEntry>>>,
}
impl<'a> ViewBindingFacts<'a> {
fn new(inner: Rc<dyn ProjectFacts + 'a>) -> Self {
Self {
inner,
probes: Default::default(),
consultations: Default::default(),
memo_stack: Default::default(),
memo_traces: Default::default(),
workspace_packages: Default::default(),
workspace_members: Default::default(),
canonical_cache: Default::default(),
file_cache: Default::default(),
config_cache: Default::default(),
directory_cache: Default::default(),
}
}
fn file_fact(&self, rel: &[u8]) -> bool {
if let Some(value) = self.file_cache.borrow().get(rel) {
return *value;
}
let value = self.inner.is_file(rel);
self.file_cache.borrow_mut().insert(rel.to_vec(), value);
value
}
fn record(&self, path: &[u8]) {
if view_resolution_config(path) {
self.record(VIEW_CONFIG_MEMBERSHIP_DOMAIN.as_bytes());
return;
}
if let Ok(path) = std::str::from_utf8(path) {
let mut parts = Vec::new();
for part in path.split('/') {
match part {
"" | "." => {}
".." => {
parts.pop();
}
_ => parts.push(part),
}
}
let path = parts.join("/");
self.probes.borrow_mut().insert(path.clone());
for (_, trace) in self.memo_stack.borrow_mut().iter_mut() {
trace.probes.insert(path.clone());
}
}
}
fn take_config(&self) -> ConfigConsultations {
let mut result = std::mem::take(&mut *self.consultations.borrow_mut());
result.unattributed = result.unattributed();
result.reads.clear();
result
}
fn config_event(&self, path: &[u8], name: Option<&str>) {
let Ok(path) = std::str::from_utf8(path) else {
self.consultations.borrow_mut().unattributed = true;
return;
};
let mut event = ConfigConsultations::default();
if let Some(name) = name {
event.facts.insert((path.into(), name.into()));
} else {
event.reads.insert(path.into());
}
self.consultations.borrow_mut().extend(&event);
for (_, trace) in self.memo_stack.borrow_mut().iter_mut() {
trace.config.extend(&event);
}
}
fn take(&self) -> BTreeSet<String> {
std::mem::take(&mut *self.probes.borrow_mut())
}
}
impl ProjectFacts for ViewBindingFacts<'_> {
fn records_config_facts(&self) -> bool {
true
}
fn config_fact(&self, rel: &[u8], name: &str) {
self.record(rel);
if matches!(name, "workspaces" | "packages") {
self.record(VIEW_CONFIG_MEMBERSHIP_DOMAIN.as_bytes());
}
self.config_event(rel, Some(name));
}
fn memo_start(&self, path: &Path, kind: &str, name: &str) {
self.memo_stack.borrow_mut().push((
(path.into(), kind.into(), name.into()),
ConsultationTrace::default(),
));
}
fn memo_finish(&self, path: &Path, kind: &str, name: &str) {
let (key, trace) = self
.memo_stack
.borrow_mut()
.pop()
.expect("balanced resolver memo recording");
debug_assert_eq!(key, (path.into(), kind.into(), name.into()));
self.memo_traces.borrow_mut().insert(key, trace);
}
fn memo_replay(&self, path: &Path, kind: &str, name: &str) {
let traces = self.memo_traces.borrow();
let Some(trace) = traces.get(&(path.into(), kind.into(), name.into())) else {
self.consultations.borrow_mut().unattributed = true;
return;
};
self.consultations.borrow_mut().extend(&trace.config);
self.probes
.borrow_mut()
.extend(trace.probes.iter().cloned());
for (_, parent) in self.memo_stack.borrow_mut().iter_mut() {
parent.config.extend(&trace.config);
parent.probes.extend(trace.probes.iter().cloned());
}
}
fn workspace_package(&self, root: &Path, name: &str) -> Option<Option<std::path::PathBuf>> {
self.workspace_packages
.borrow()
.get(&(root.into(), name.into()))
.cloned()
}
fn remember_workspace_package(
&self,
root: &Path,
name: &str,
value: Option<std::path::PathBuf>,
) {
self.workspace_packages
.borrow_mut()
.insert((root.into(), name.into()), value);
}
fn workspace_members(&self, root: &Path) -> Option<Arc<Vec<std::path::PathBuf>>> {
self.workspace_members.borrow().get(root).cloned()
}
fn remember_workspace_members(&self, root: &Path, value: Arc<Vec<std::path::PathBuf>>) {
self.workspace_members
.borrow_mut()
.insert(root.into(), value);
}
fn is_file(&self, rel: &[u8]) -> bool {
let is_file = self.file_fact(rel);
if is_file {
self.record(rel);
}
is_file
}
fn is_dir(&self, rel: &[u8]) -> bool {
self.inner.is_dir(rel)
}
fn config_bytes(&self, rel: &[u8]) -> Option<Arc<[u8]>> {
self.consultations.borrow_mut().unattributed = true;
for (_, trace) in self.memo_stack.borrow_mut().iter_mut() {
trace.config.unattributed = true;
}
self.attributed_config_bytes(rel)
}
fn attributed_config_bytes(&self, rel: &[u8]) -> Option<Arc<[u8]>> {
self.config_event(rel, None);
self.record(rel);
if let Some(value) = self.config_cache.borrow().get(rel) {
return value.clone();
}
let value = self.inner.config_bytes(rel);
self.config_cache
.borrow_mut()
.insert(rel.to_vec(), value.clone());
value
}
fn symlink_target(&self, rel: &[u8]) -> Option<&[u8]> {
self.inner.symlink_target(rel)
}
fn canonical(&self, rel: &[u8]) -> Option<Vec<u8>> {
let cached = self.canonical_cache.borrow().get(rel).cloned();
let canonical = cached.unwrap_or_else(|| {
let value = self.inner.canonical(rel);
self.canonical_cache
.borrow_mut()
.insert(rel.to_vec(), value.clone());
value
});
if canonical.as_ref().is_none_or(|path| self.file_fact(path)) {
self.record(rel);
}
canonical
}
fn list_dir(&self, rel: &[u8]) -> Vec<super::facts::DirEntry> {
if let Some(value) = self.directory_cache.borrow().get(rel) {
return value.clone();
}
let value = self.inner.list_dir(rel);
self.directory_cache
.borrow_mut()
.insert(rel.to_vec(), value.clone());
value
}
}
#[cfg(test)]
pub(crate) fn join_selected_manifest(
manifest: &Manifest,
blobs: &impl ManifestBlobReader,
selected: Option<&BTreeSet<String>>,
cached: &BTreeMap<String, ViewBindingDependencies>,
) -> Result<SelectedManifestJoin, ManifestJoinError> {
join_manifest_with_surfaces(manifest, blobs, selected, cached, None)
}
pub(crate) fn join_selected_manifest_reusing_surfaces(
manifest: &Manifest,
blobs: &impl ManifestBlobReader,
selected: Option<&BTreeSet<String>>,
cached: &BTreeMap<String, ViewBindingDependencies>,
changed: &BTreeSet<String>,
membership_changed: &BTreeSet<String>,
fact_invalidated: &BTreeSet<String>,
) -> Result<SelectedManifestJoin, ManifestJoinError> {
join_manifest_with_surfaces(
manifest,
blobs,
selected,
cached,
Some((changed, membership_changed, fact_invalidated)),
)
}
fn join_manifest_with_surfaces(
manifest: &Manifest,
blobs: &impl ManifestBlobReader,
selected: Option<&BTreeSet<String>>,
cached: &BTreeMap<String, ViewBindingDependencies>,
reuse: Option<(&BTreeSet<String>, &BTreeSet<String>, &BTreeSet<String>)>,
) -> Result<SelectedManifestJoin, ManifestJoinError> {
let mut profile = crate::views::materialization::profile::PhaseTimer::new("join");
let loaded = std::cell::RefCell::new(BTreeMap::<BlobKey, Arc<[u8]>>::new());
let load = |key: &BlobKey| -> Result<Arc<[u8]>, ManifestJoinError> {
if let Some(bytes) = loaded.borrow().get(key) {
return Ok(bytes.clone());
}
let bytes: Arc<[u8]> = blobs
.read_callgraph_blob(key)?
.ok_or_else(|| ManifestJoinError::MissingBlob(key.clone()))?
.into();
loaded.borrow_mut().insert(key.clone(), bytes.clone());
Ok(bytes)
};
let read_error = std::cell::RefCell::new(None);
let reader = |key: &BlobKey| match load(key) {
Ok(bytes) => Some(bytes),
Err(error) => {
*read_error.borrow_mut() = Some(error);
None
}
};
profile.finish("load_payloads");
let facts = Rc::new(ViewBindingFacts::new(Rc::new(ManifestFacts {
manifest,
blobs: &reader,
})));
let root = Path::new("/");
let paths = FactPaths {
root,
facts: facts.as_ref(),
};
let mut extracts = HashMap::new();
let mut files = HashMap::new();
let mut work = Vec::new();
let mut bindings = BTreeMap::new();
let mut unbound_non_utf8_paths = Vec::new();
let mut rebuilt_surface_entries = 0;
let mut decoded_caller_blobs = 0;
for (path, entry) in manifest.entries() {
let ManifestEntry::Regular { planes, .. } = entry else {
continue;
};
let Some(key) = &planes.callgraph else {
continue;
};
let Ok(rel) = std::str::from_utf8(path.as_bytes()) else {
if matches!(
CallgraphBlob::from_bytes(&load(key)?)?,
CallgraphBlob::Parse(_)
) {
unbound_non_utf8_paths.push(path.as_bytes().to_vec());
}
continue;
};
let resolve = selected.is_none_or(|set| set.contains(rel));
let cache = cached.get(rel).filter(|cache| {
if let Some((changed, membership, fact_invalidated)) = reuse {
selected.is_some()
&& !changed.contains(rel)
&& !fact_invalidated.contains(rel)
&& cache.dependencies.is_disjoint(membership)
} else {
!resolve
}
});
if let Some((cache, surface)) =
cache.and_then(|cache| cache.surface.as_ref().map(|surface| (cache, surface)))
{
files.insert(rel.to_string(), surface.restore());
bindings.insert(rel.to_string(), cache.clone());
continue;
}
let CallgraphBlob::Parse(blob) = CallgraphBlob::from_bytes(&load(key)?)? else {
continue;
};
decoded_caller_blobs += 1;
facts.take();
facts.take_config();
let extract =
blob.bind_with_dependencies(rel, &paths, cache.map(|cache| &cache.references))?;
let file_index = super::DbFileIndex::from_extract(root, &extract, &paths);
rebuilt_surface_entries += 1;
let mut binding = cache.cloned().unwrap_or_default();
binding.surface = Some(ViewFileSurface::capture(&blob.language, &file_index));
files.insert(rel.to_string(), file_index);
if cache.is_none() {
binding.references = blob
.refs
.iter()
.zip(&extract.raw_refs)
.enumerate()
.map(|(position, (_, bound))| {
(
u32::try_from(position).expect("reference vector fits u32"),
bound.dependencies.clone(),
)
})
.collect();
binding.binding_probes = facts.take();
binding.binding_facts = facts.take_config();
binding.dependencies = binding.references.values().flatten().cloned().collect();
binding
.dependencies
.extend(binding.binding_probes.iter().cloned());
}
bindings.insert(rel.to_string(), binding);
if resolve {
for (raw, bound) in blob.refs.iter().zip(&extract.raw_refs) {
work.push((
CallerRefKey {
caller_blob_key: key.clone(),
ref_ordinal: raw.ordinal,
caller_path: path.as_bytes().to_vec(),
},
(raw.kind, bound.clone()),
));
}
}
extracts.insert(rel.to_string(), extract);
}
profile.finish("decode_bind_index_entries");
let index = ManifestProjectIndex::from_parts(
root,
files,
HashMap::new(),
super::WorkspaceCratePrefixCache::default(),
facts.clone(),
);
let mut result = JoinResult {
rows: BTreeSet::new(),
resolution_order: Vec::new(),
unbound_non_utf8_paths,
};
let resolved_callers = bindings
.keys()
.filter(|path| {
if selected.is_some_and(|set| !set.contains(*path)) {
return false;
}
let Some((changed, _, _)) = reuse else {
return true;
};
if selected.is_none() || changed.contains(*path) {
return true;
}
cached.get(*path).is_none_or(|old| {
old.references != bindings[*path].references
|| old
.surface_queries
.iter()
.any(|(query, expected)| query.answer(&index) != *expected)
})
})
.cloned()
.collect::<BTreeSet<_>>();
for (path, binding) in &mut bindings {
if resolved_callers.contains(path) {
binding.resolved_dependencies.clear();
binding.surface_queries.clear();
binding.resolution_facts = ConfigConsultations::default();
} else if let Some(old) = cached.get(path) {
binding.resolved_dependencies = old.resolved_dependencies.clone();
binding.surface_queries = old.surface_queries.clone();
}
}
profile.finish("index_and_surface_replay");
for caller in &resolved_callers {
if extracts.contains_key(caller) {
continue;
}
let path = RelPath::new(caller.as_bytes().to_vec()).expect("bound manifest path");
let Some(ManifestEntry::Regular { planes, .. }) = manifest.get(&path) else {
continue;
};
let key = planes.callgraph.as_ref().expect("bound caller key");
let CallgraphBlob::Parse(blob) = CallgraphBlob::from_bytes(&load(key)?)? else {
continue;
};
decoded_caller_blobs += 1;
facts.take();
facts.take_config();
let extract =
blob.bind_with_dependencies(caller, &paths, Some(&bindings[caller].references))?;
for (raw, bound) in blob.refs.iter().zip(&extract.raw_refs) {
work.push((
CallerRefKey {
caller_blob_key: key.clone(),
ref_ordinal: raw.ordinal,
caller_path: caller.as_bytes().to_vec(),
},
(raw.kind, bound.clone()),
));
}
extracts.insert(caller.clone(), extract);
}
let index = ManifestProjectIndex::from_parts(
root,
index.files,
extracts
.iter()
.map(|(path, extract)| (path.clone(), &extract.data))
.collect(),
super::WorkspaceCratePrefixCache::default(),
facts.clone(),
);
profile.finish("decode_resolved_callers");
let surface_index = ViewSurfaceIndex {
inner: &index,
queries: Default::default(),
};
let mut queries = BTreeMap::<String, BTreeMap<ViewSurfaceQuery, String>>::new();
let bases: BTreeMap<_, BTreeSet<_>> = resolved_callers
.iter()
.map(|caller| {
let binding = &bindings[caller];
(
caller.clone(),
binding
.references
.values()
.flatten()
.chain(binding.binding_probes.iter())
.cloned()
.collect(),
)
})
.collect();
let mut resolutions = HashMap::<ViewResolutionBinding, (Option<String>, Option<String>)>::new();
work.sort_by(|a, b| (&a.0, a.1 .0).cmp(&(&b.0, b.1 .0)));
for (key, (kind, raw)) in work {
let caller = std::str::from_utf8(&key.caller_path).expect("bound UTF-8 caller");
if !resolved_callers.contains(caller) {
continue;
}
let memo_key = ViewResolutionBinding::new(&raw, &extracts[caller].data);
let binding = bindings.get_mut(caller).expect("bound caller dependencies");
let basis = &bases[caller];
binding.resolved_dependencies.extend(
raw.dependencies
.iter()
.filter(|dependency| !basis.contains(*dependency))
.cloned(),
);
let (target_file, target_symbol) = match resolutions.entry(memo_key) {
std::collections::hash_map::Entry::Occupied(entry) => entry.get().clone(),
std::collections::hash_map::Entry::Vacant(entry) => {
facts.take();
facts.take_config();
let resolved = super::resolve_ref(raw, &surface_index)
.map_err(|error| ManifestJoinError::Parse(error.to_string()))?;
binding.resolution_facts.extend(&facts.take_config());
queries
.entry(caller.to_string())
.or_default()
.extend(surface_index.take());
binding.resolved_dependencies.extend(
resolved
.dependencies
.into_iter()
.chain(facts.take())
.filter(|dependency| !basis.contains(dependency)),
);
entry
.insert((resolved.target_file, resolved.target_symbol))
.clone()
}
};
result.rows.insert(DerivedRow {
caller_blob_key: key.caller_blob_key.clone(),
ref_ordinal: key.ref_ordinal,
caller_path: key.caller_path.clone(),
kind,
status: if target_file.is_some() {
ResolutionStatus::Resolved
} else {
ResolutionStatus::Unresolved
},
target_path: target_file.map(String::into_bytes),
target_symbol,
});
result.resolution_order.push(key);
}
profile.finish("resolve_and_record");
for (path, binding) in &mut bindings {
binding.consulted_facts = binding
.binding_facts
.facts
.union(&binding.resolution_facts.facts)
.cloned()
.collect();
binding.unattributed =
binding.binding_facts.unattributed() || binding.resolution_facts.unattributed();
if let Some(queries) = queries.remove(path) {
binding.surface_queries = queries.into_iter().collect();
}
binding.dependencies = binding
.references
.values()
.flatten()
.cloned()
.chain(binding.binding_probes.iter().cloned())
.chain(binding.resolved_dependencies.iter().cloned())
.chain(
binding
.surface_queries
.iter()
.flat_map(|(query, _)| query.dependencies()),
)
.collect();
}
profile.finish("dependency_union");
if let Some(error) = read_error.borrow_mut().take() {
return Err(error);
}
Ok(SelectedManifestJoin {
result,
bindings,
resolved_callers,
rebuilt_surface_entries,
decoded_caller_blobs,
resolved_bindings: resolutions.len(),
})
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
enum ViewSurfaceQuery {
Language(String),
Module(String, String),
Parent(String),
Reexports(String),
Node(String, String),
Callable(String, String),
Alias(String, String),
Export(String, String),
Default(String),
Contains(String),
Crate(String),
CrateRoot(String),
Inline(String, Vec<String>, String),
}
fn surface_value(value: &impl Serialize) -> String {
blake3::hash(&serde_json::to_vec(value).expect("resolver surface serializes"))
.to_hex()
.to_string()
}
fn reexport_surface(value: &[super::ReexportIndex]) -> String {
surface_value(
&value
.iter()
.map(|entry| {
(
&entry.target_file,
entry.named.iter().collect::<BTreeMap<_, _>>(),
entry.wildcard,
)
})
.collect::<Vec<_>>(),
)
}
pub(crate) const VIEW_CONFIG_MEMBERSHIP_DOMAIN: &str = "\0view:config-membership";
pub(crate) const VIEW_RUST_MODULE_DOMAIN: &str = "\0view:rust-module-index";
impl ViewSurfaceQuery {
fn dependencies(&self) -> Vec<String> {
match self {
Self::Crate(_) => vec![VIEW_CONFIG_MEMBERSHIP_DOMAIN.into()],
Self::CrateRoot(file) => {
vec![file.clone(), VIEW_CONFIG_MEMBERSHIP_DOMAIN.into()]
}
Self::Parent(file) | Self::Inline(file, ..) => {
vec![file.clone(), VIEW_RUST_MODULE_DOMAIN.into()]
}
Self::Language(file)
| Self::Module(file, _)
| Self::Reexports(file)
| Self::Node(file, _)
| Self::Callable(file, _)
| Self::Alias(file, _)
| Self::Export(file, _)
| Self::Default(file)
| Self::Contains(file) => vec![file.clone()],
}
}
fn answer(&self, index: &impl super::ResolverIndex) -> String {
match self {
Self::Language(file) => {
surface_value(&index.lang_for(file).map(|lang| format!("{lang:?}")))
}
Self::Module(file, module) => surface_value(&index.module_target(file, module)),
Self::Parent(file) => surface_value(&index.module_parent(file)),
Self::Reexports(file) => reexport_surface(&index.reexports_for(file)),
Self::Node(file, symbol) => surface_value(&index.node_for_symbol(file, symbol)),
Self::Callable(file, node) => surface_value(&index.node_is_callable(file, node)),
Self::Alias(file, symbol) => surface_value(&index.export_alias(file, symbol)),
Self::Export(file, symbol) => surface_value(&index.has_export(file, symbol)),
Self::Default(file) => surface_value(&index.default_export(file)),
Self::Contains(file) => surface_value(&index.contains_file(file)),
Self::Crate(name) => surface_value(&index.crate_src_prefix(name)),
Self::CrateRoot(file) => surface_value(&index.rust_crate_root_file(file)),
Self::Inline(file, segments, symbol) => {
surface_value(&index.inline_scoped_target(file, segments, symbol))
}
}
}
}
struct ViewSurfaceIndex<'a, I> {
inner: &'a I,
queries: std::cell::RefCell<BTreeMap<ViewSurfaceQuery, String>>,
}
impl<I> ViewSurfaceIndex<'_, I> {
fn record(&self, query: ViewSurfaceQuery, value: &impl Serialize) {
self.queries
.borrow_mut()
.insert(query, surface_value(value));
}
fn take(&self) -> BTreeMap<ViewSurfaceQuery, String> {
std::mem::take(&mut *self.queries.borrow_mut())
}
}
impl<I: super::ResolverIndex> super::ResolverIndex for ViewSurfaceIndex<'_, I> {
fn caller_data(&self, file: &str) -> Option<&FileCallData> {
self.inner.caller_data(file)
}
fn lang_for(&self, file: &str) -> Option<LangId> {
let value = self.inner.lang_for(file);
self.record(
ViewSurfaceQuery::Language(file.into()),
&value.map(|lang| format!("{lang:?}")),
);
value
}
fn module_target(&self, file: &str, module: &str) -> Option<String> {
let value = self.inner.module_target(file, module);
self.record(ViewSurfaceQuery::Module(file.into(), module.into()), &value);
value
}
fn module_parent(&self, file: &str) -> Option<(String, String)> {
let value = self.inner.module_parent(file);
self.record(ViewSurfaceQuery::Parent(file.into()), &value);
value
}
fn reexports_for(&self, file: &str) -> Vec<super::ReexportIndex> {
let value = self.inner.reexports_for(file);
self.queries.borrow_mut().insert(
ViewSurfaceQuery::Reexports(file.into()),
reexport_surface(&value),
);
value
}
fn node_for_symbol(&self, file: &str, symbol: &str) -> Option<String> {
let value = self.inner.node_for_symbol(file, symbol);
self.record(ViewSurfaceQuery::Node(file.into(), symbol.into()), &value);
value
}
fn node_is_callable(&self, file: &str, node: &str) -> bool {
let value = self.inner.node_is_callable(file, node);
self.record(ViewSurfaceQuery::Callable(file.into(), node.into()), &value);
value
}
fn export_alias(&self, file: &str, symbol: &str) -> Option<String> {
let value = self.inner.export_alias(file, symbol);
self.record(ViewSurfaceQuery::Alias(file.into(), symbol.into()), &value);
value
}
fn has_export(&self, file: &str, symbol: &str) -> bool {
let value = self.inner.has_export(file, symbol);
self.record(ViewSurfaceQuery::Export(file.into(), symbol.into()), &value);
value
}
fn default_export(&self, file: &str) -> Option<String> {
let value = self.inner.default_export(file);
self.record(ViewSurfaceQuery::Default(file.into()), &value);
value
}
fn contains_file(&self, file: &str) -> bool {
let value = self.inner.contains_file(file);
self.record(ViewSurfaceQuery::Contains(file.into()), &value);
value
}
fn crate_src_prefix(&self, name: &str) -> Option<String> {
let value = self.inner.crate_src_prefix(name);
self.record(ViewSurfaceQuery::Crate(name.into()), &value);
value
}
fn rust_crate_root_file(&self, file: &str) -> Option<String> {
let value = self.inner.rust_crate_root_file(file);
self.record(ViewSurfaceQuery::CrateRoot(file.into()), &value);
value
}
fn inline_scoped_target(
&self,
file: &str,
segments: &[String],
symbol: &str,
) -> Option<(String, String)> {
let value = self.inner.inline_scoped_target(file, segments, symbol);
self.record(
ViewSurfaceQuery::Inline(file.into(), segments.to_vec(), symbol.into()),
&value,
);
value
}
}
#[cfg(test)]
mod consultation_tests {
use super::*;
#[test]
fn opaque_config_read_is_unattributed_even_after_a_known_field_read() {
let manifest = Manifest::new([(
RelPath::new(b"package.json".to_vec()).unwrap(),
ManifestEntry::Regular {
mode: 0o100644,
planes: crate::views::RegularPlanes {
callgraph: Some("config".into()),
semantic: None,
},
resolution_input: true,
},
)])
.unwrap();
let bytes: Arc<[u8]> = CallgraphBlob::config(b"{}".to_vec(), "fixture")
.to_bytes()
.unwrap()
.into();
let reader = |_: &BlobKey| Some(bytes.clone());
let facts = ViewBindingFacts::new(Rc::new(ManifestFacts {
manifest: &manifest,
blobs: &reader,
}));
facts.config_fact(b"package.json", "name");
assert!(facts.attributed_config_bytes(b"package.json").is_some());
assert!(!facts.consultations.borrow().unattributed());
facts.memo_start(Path::new("/"), "test", "");
assert!(facts.config_bytes(b"package.json").is_some());
facts.memo_finish(Path::new("/"), "test", "");
assert!(facts.take_config().unattributed());
facts.memo_replay(Path::new("/"), "test", "");
assert!(facts.take_config().unattributed());
}
}