use std::collections::HashMap;
use std::path::Path;
use serde_json::Value;
use crate::graph::{read_compiler_facts, CompilationTally};
use crate::resolve::DefIndex;
use super::freshness::{self, Freshness};
use super::uncertainty::{self, Uncertainty};
#[derive(Debug, Clone)]
struct RawOccurrence {
file: String,
name_line: usize,
resolution: String,
target_type: Option<String>,
target_member: Option<String>,
compilation_identity: Value,
#[allow(
dead_code,
reason = "the exact identity this consumer's own model carries alongside the compatibility key, parsed for a future consumer (an exact-identity export, a disagreement diagnostic naming the exact overload) -- target_overload_signature is the one field lookup reads today, for its own arity narrowing and target identity"
)]
target_assembly: Option<String>,
#[allow(dead_code, reason = "see target_assembly's own allow above")]
target_generic_arity: Option<u64>,
target_overload_signature: Option<String>,
#[allow(dead_code, reason = "see target_assembly's own allow above")]
span_start_line: Option<u64>,
#[allow(dead_code, reason = "see target_assembly's own allow above")]
span_start_char: Option<u64>,
#[allow(dead_code, reason = "see target_assembly's own allow above")]
span_end_line: Option<u64>,
#[allow(dead_code, reason = "see target_assembly's own allow above")]
span_end_char: Option<u64>,
#[allow(dead_code, reason = "see target_assembly's own allow above")]
document_content_identity: Option<String>,
#[allow(dead_code, reason = "see target_assembly's own allow above")]
target_document_content_identities: Vec<String>,
}
#[derive(Debug, Clone)]
struct RawCompilation {
identity: Value,
state: String,
reason: String,
imports: Vec<freshness::ImportRef>,
}
fn parse_imports(entry: &serde_json::Map<String, Value>) -> Vec<freshness::ImportRef> {
let Some(raw) = entry.get("imports").and_then(Value::as_array) else {
return Vec::new();
};
raw.iter()
.filter_map(|item| {
let obj = as_obj(item)?;
Some(freshness::ImportRef {
identity: obj.get("identity").and_then(Value::as_str)?.to_string(),
hash: obj.get("hash").and_then(Value::as_str)?.to_string(),
})
})
.collect()
}
struct RawFacts {
source_head_sha: Option<String>,
source_dirty: bool,
compilations: Vec<RawCompilation>,
tally: CompilationTally,
occurrences: Vec<RawOccurrence>,
}
fn as_obj(v: &Value) -> Option<&serde_json::Map<String, Value>> {
v.as_object()
}
fn parse_compilations(root: &Value) -> Vec<RawCompilation> {
let Some(entries) = root
.get("context")
.and_then(as_obj)
.and_then(|c| c.get("envelope"))
.and_then(as_obj)
.and_then(|e| e.get("compilations"))
.and_then(Value::as_array)
else {
return Vec::new();
};
entries
.iter()
.filter_map(|entry| {
let obj = as_obj(entry)?;
Some(RawCompilation {
identity: obj.get("identity")?.clone(),
state: obj.get("state").and_then(Value::as_str)?.to_string(),
reason: obj
.get("reason")
.and_then(Value::as_str)
.unwrap_or("")
.to_string(),
imports: parse_imports(obj),
})
})
.collect()
}
fn parse_occurrences(root: &Value) -> Vec<RawOccurrence> {
let Some(sites) = root
.get("occurrences")
.and_then(as_obj)
.and_then(|o| o.get("sites"))
.and_then(Value::as_array)
else {
return Vec::new();
};
sites
.iter()
.filter_map(|site| {
let obj = as_obj(site)?;
let file = obj.get("file").and_then(Value::as_str)?.to_string();
let name_line = obj
.get("name")
.and_then(as_obj)
.and_then(|n| n.get("line"))
.and_then(Value::as_u64)? as usize;
let resolution = obj.get("resolution").and_then(Value::as_str)?.to_string();
let target = obj.get("target").and_then(as_obj);
let target_type = target
.and_then(|t| t.get("type"))
.and_then(Value::as_str)
.map(str::to_string);
let target_member = target
.and_then(|t| t.get("member"))
.and_then(Value::as_str)
.map(str::to_string);
let compilation_identity = obj
.get("compilation")
.and_then(as_obj)
.and_then(|c| c.get("identity"))
.cloned()
.unwrap_or(Value::Null);
let target_assembly = target
.and_then(|t| t.get("assembly"))
.and_then(Value::as_str)
.map(str::to_string);
let target_generic_arity = target
.and_then(|t| t.get("genericArity"))
.and_then(Value::as_u64);
let target_overload_signature = target
.and_then(|t| t.get("overloadSignature"))
.and_then(Value::as_str)
.map(str::to_string);
let span = obj.get("span").and_then(as_obj);
let span_start_line = span
.and_then(|s| s.get("startLine"))
.and_then(Value::as_u64);
let span_start_char = span
.and_then(|s| s.get("startChar"))
.and_then(Value::as_u64);
let span_end_line = span.and_then(|s| s.get("endLine")).and_then(Value::as_u64);
let span_end_char = span.and_then(|s| s.get("endChar")).and_then(Value::as_u64);
let document_content_identity = obj
.get("documentContentIdentity")
.and_then(Value::as_str)
.map(str::to_string);
let target_document_content_identities = obj
.get("targetDocumentContentIdentities")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
.unwrap_or_default();
Some(RawOccurrence {
file,
name_line,
resolution,
target_type,
target_member,
compilation_identity,
target_assembly,
target_generic_arity,
target_overload_signature,
span_start_line,
span_start_char,
span_end_line,
span_end_char,
document_content_identity,
target_document_content_identities,
})
})
.collect()
}
fn overload_arg_count(signature: &str) -> Option<usize> {
let after_open = signature.strip_prefix('(')?;
let close = after_open.find(")->")?;
let params = &after_open[..close];
if params.trim().is_empty() {
return Some(0);
}
let mut count = 1;
let mut depth = 0i32;
for ch in params.chars() {
match ch {
'<' | '(' | '[' => depth += 1,
'>' | ')' | ']' => depth -= 1,
',' if depth == 0 => count += 1,
_ => {}
}
}
Some(count)
}
fn parse_raw(bytes: &[u8]) -> Option<RawFacts> {
let root: Value = serde_json::from_slice(bytes).ok()?;
let source_head_sha = root
.get("sourceSnapshot")
.and_then(as_obj)
.and_then(|s| s.get("headSha"))
.and_then(Value::as_str)
.map(str::to_string);
let source_dirty = root
.get("sourceSnapshot")
.and_then(as_obj)
.and_then(|s| s.get("dirty"))
.and_then(Value::as_bool)
.unwrap_or(true);
Some(RawFacts {
source_head_sha,
source_dirty,
compilations: parse_compilations(&root),
tally: CompilationTally::from_artifact(&root),
occurrences: parse_occurrences(&root),
})
}
fn split_qualified_type(raw: &str) -> Vec<String> {
let mut segments = Vec::new();
let mut current = String::new();
let mut depth = 0i32;
for ch in raw.chars() {
match ch {
'<' => {
depth += 1;
current.push(ch);
}
'>' => {
depth -= 1;
current.push(ch);
}
'.' if depth == 0 => {
segments.push(current.clone());
current.clear();
}
_ => current.push(ch),
}
}
if !current.is_empty() {
segments.push(current);
}
segments
.into_iter()
.map(|seg| match seg.find('<') {
Some(i) => seg[..i].to_string(),
None => seg,
})
.collect()
}
fn translate_type_id(raw: &str, index: &DefIndex) -> Option<usize> {
let segments = split_qualified_type(raw);
for k in 1..=segments.len() {
let head = segments[..k].join(".");
let Some(&start) = index.qualified_name_to_def.get(&head) else {
continue;
};
let mut cur = start;
let mut complete = true;
for seg in &segments[k..] {
let candidate = format!("{}+{}", index.defs[cur].id, seg);
match index.qualified_name_to_def.get(&candidate) {
Some(&next) => cur = next,
None => {
complete = false;
break;
}
}
}
if complete {
return Some(cur);
}
}
None
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemanticTarget {
pub to_def_id: String,
pub to_file: String,
pub overload_signature: Option<String>,
}
fn resolve_target(
type_idx: usize,
member: &str,
overload_signature: Option<&str>,
index: &DefIndex,
) -> SemanticTarget {
let def = &index.defs[type_idx];
let overload_signature = overload_signature.map(str::to_string);
if def.kind == "enum" {
let key = format!("{}.{member}", def.id);
if let Some(&member_idx) = index.qualified_name_to_def.get(&key) {
let member_def = &index.defs[member_idx];
return SemanticTarget {
to_def_id: member_def.id.clone(),
to_file: member_def.file.clone(),
overload_signature,
};
}
}
SemanticTarget {
to_def_id: def.id.clone(),
to_file: def.file.clone(),
overload_signature,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LookupOutcome {
NoFact,
Confirmed(SemanticTarget),
Other(Uncertainty),
ConfirmedMany(Vec<SemanticTarget>),
}
pub struct SemanticLayer {
freshness: Freshness,
by_site: HashMap<(String, usize, String), Vec<usize>>,
occurrences: Vec<RawOccurrence>,
compilations: Vec<RawCompilation>,
tally: CompilationTally,
}
impl SemanticLayer {
pub fn load(root: &Path) -> Option<SemanticLayer> {
let admitted = read_compiler_facts(root)?;
let raw = parse_raw(&admitted.bytes)?;
let imports: Vec<freshness::ImportRef> = raw
.compilations
.iter()
.flat_map(|c| c.imports.iter().cloned())
.collect();
let freshness = freshness::evaluate(
root,
raw.source_head_sha.as_deref(),
raw.source_dirty,
&imports,
);
let mut by_site: HashMap<(String, usize, String), Vec<usize>> = HashMap::new();
for (i, occ) in raw.occurrences.iter().enumerate() {
if let Some(member) = &occ.target_member {
by_site
.entry((occ.file.clone(), occ.name_line, member.clone()))
.or_default()
.push(i);
}
}
Some(SemanticLayer {
freshness,
by_site,
occurrences: raw.occurrences,
compilations: raw.compilations,
tally: raw.tally,
})
}
pub fn is_fresh(&self) -> bool {
self.freshness.is_fresh()
}
pub fn compilation_tally(&self) -> &CompilationTally {
&self.tally
}
pub fn freshness(&self) -> &Freshness {
&self.freshness
}
pub fn site_keys(&self) -> impl Iterator<Item = &(String, usize, String)> {
self.by_site.keys()
}
fn compilation_state(&self, identity: &Value) -> Option<(&str, &str)> {
self.compilations
.iter()
.find(|c| &c.identity == identity)
.map(|c| (c.state.as_str(), c.reason.as_str()))
}
pub fn lookup(
&self,
index: &DefIndex,
file: &str,
line: usize,
member: &str,
caller_arg_count: Option<usize>,
) -> LookupOutcome {
if !self.freshness.is_fresh() {
let Freshness::Stale { reason } = &self.freshness else {
unreachable!("is_fresh() already returned false");
};
return LookupOutcome::Other(Uncertainty::Stale {
reason: reason.clone(),
});
}
let Some(indices) = self
.by_site
.get(&(file.to_string(), line, member.to_string()))
else {
return LookupOutcome::NoFact;
};
let narrowed: Vec<usize>;
let indices: &[usize] = if indices.len() > 1 {
if let Some(want) = caller_arg_count {
let matching: Vec<usize> = indices
.iter()
.copied()
.filter(|&i| {
self.occurrences[i]
.target_overload_signature
.as_deref()
.and_then(overload_arg_count)
== Some(want)
})
.collect();
if matching.len() == 1 {
narrowed = matching;
&narrowed
} else {
indices.as_slice()
}
} else {
indices.as_slice()
}
} else {
indices.as_slice()
};
let mut resolved_targets: Vec<SemanticTarget> = Vec::new();
let mut other: Option<Uncertainty> = None;
for &i in indices {
let occ = &self.occurrences[i];
let state = self.compilation_state(&occ.compilation_identity);
let uncertainty = match state {
Some((cstate, creason)) => uncertainty::from_compilation_state(cstate, creason)
.unwrap_or_else(|| uncertainty::from_resolution(&occ.resolution)),
None => uncertainty::from_resolution(&occ.resolution),
};
if uncertainty.is_confirmed() {
if let Some(type_id) = occ.target_type.as_deref() {
if let Some(type_idx) = translate_type_id(type_id, index) {
resolved_targets.push(resolve_target(
type_idx,
member,
occ.target_overload_signature.as_deref(),
index,
));
continue;
}
}
other.get_or_insert(Uncertainty::Candidate);
} else {
other.get_or_insert(uncertainty);
}
}
resolved_targets.dedup();
match resolved_targets.len() {
0 => other
.map(LookupOutcome::Other)
.unwrap_or(LookupOutcome::NoFact),
1 => LookupOutcome::Confirmed(resolved_targets.remove(0)),
_ => LookupOutcome::ConfirmedMany(resolved_targets),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn overload_arg_count_reads_a_zero_arg_signature() {
assert_eq!(overload_arg_count("()->void"), Some(0));
}
#[test]
fn overload_arg_count_counts_top_level_commas() {
assert_eq!(overload_arg_count("(bool, string)->void"), Some(2));
}
#[test]
fn overload_arg_count_never_splits_inside_a_generic_argument_list() {
assert_eq!(
overload_arg_count("(System.Collections.Generic.Dictionary<string, int>)->void"),
Some(1)
);
}
#[test]
fn overload_arg_count_is_none_for_a_string_with_no_parenthesized_shape() {
assert_eq!(overload_arg_count("void"), None);
assert_eq!(overload_arg_count(""), None);
}
#[test]
fn splits_plain_dotted_segments() {
assert_eq!(
split_qualified_type("Ns.Outer.Widget"),
vec!["Ns", "Outer", "Widget"]
);
}
#[test]
fn strips_a_trailing_generic_suffix_per_segment() {
assert_eq!(
split_qualified_type("Ns.Outer.Nested<T>"),
vec!["Ns", "Outer", "Nested"]
);
}
#[test]
fn never_splits_inside_a_generic_argument_list() {
assert_eq!(
split_qualified_type("Ns.Holder<System.String>.Leaf"),
vec!["Ns", "Holder", "Leaf"]
);
}
#[test]
fn a_doubly_nested_generic_strips_every_level() {
assert_eq!(
split_qualified_type("Ns.Outer<K>.Middle<V>.Leaf"),
vec!["Ns", "Outer", "Middle", "Leaf"]
);
}
}