1use std::collections::HashMap;
23use std::path::Path;
24
25use serde_json::Value;
26
27use crate::graph::read_compiler_facts;
28use crate::resolve::DefIndex;
29
30use super::freshness::{self, Freshness};
31use super::uncertainty::{self, Uncertainty};
32
33#[derive(Debug, Clone)]
53struct RawOccurrence {
54 file: String,
55 name_line: usize,
56 resolution: String,
57 target_type: Option<String>,
58 target_member: Option<String>,
59 compilation_identity: Value,
60 #[allow(
61 dead_code,
62 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"
63 )]
64 target_assembly: Option<String>,
65 #[allow(dead_code, reason = "see target_assembly's own allow above")]
66 target_generic_arity: Option<u64>,
67 target_overload_signature: Option<String>,
68 #[allow(dead_code, reason = "see target_assembly's own allow above")]
69 span_start_line: Option<u64>,
70 #[allow(dead_code, reason = "see target_assembly's own allow above")]
71 span_start_char: Option<u64>,
72 #[allow(dead_code, reason = "see target_assembly's own allow above")]
73 span_end_line: Option<u64>,
74 #[allow(dead_code, reason = "see target_assembly's own allow above")]
75 span_end_char: Option<u64>,
76 #[allow(dead_code, reason = "see target_assembly's own allow above")]
77 document_content_identity: Option<String>,
78 #[allow(dead_code, reason = "see target_assembly's own allow above")]
79 target_document_content_identities: Vec<String>,
80}
81
82#[derive(Debug, Clone)]
89struct RawCompilation {
90 identity: Value,
91 state: String,
92 reason: String,
93 imports: Vec<freshness::ImportRef>,
94}
95
96fn parse_imports(entry: &serde_json::Map<String, Value>) -> Vec<freshness::ImportRef> {
97 let Some(raw) = entry.get("imports").and_then(Value::as_array) else {
98 return Vec::new();
99 };
100 raw.iter()
101 .filter_map(|item| {
102 let obj = as_obj(item)?;
103 Some(freshness::ImportRef {
104 identity: obj.get("identity").and_then(Value::as_str)?.to_string(),
105 hash: obj.get("hash").and_then(Value::as_str)?.to_string(),
106 })
107 })
108 .collect()
109}
110
111struct RawFacts {
115 source_head_sha: Option<String>,
116 source_dirty: bool,
117 compilations: Vec<RawCompilation>,
118 occurrences: Vec<RawOccurrence>,
119}
120
121fn as_obj(v: &Value) -> Option<&serde_json::Map<String, Value>> {
122 v.as_object()
123}
124
125fn parse_compilations(root: &Value) -> Vec<RawCompilation> {
126 let Some(entries) = root
127 .get("context")
128 .and_then(as_obj)
129 .and_then(|c| c.get("envelope"))
130 .and_then(as_obj)
131 .and_then(|e| e.get("compilations"))
132 .and_then(Value::as_array)
133 else {
134 return Vec::new();
135 };
136 entries
137 .iter()
138 .filter_map(|entry| {
139 let obj = as_obj(entry)?;
140 Some(RawCompilation {
141 identity: obj.get("identity")?.clone(),
142 state: obj.get("state").and_then(Value::as_str)?.to_string(),
143 reason: obj
144 .get("reason")
145 .and_then(Value::as_str)
146 .unwrap_or("")
147 .to_string(),
148 imports: parse_imports(obj),
149 })
150 })
151 .collect()
152}
153
154fn parse_occurrences(root: &Value) -> Vec<RawOccurrence> {
155 let Some(sites) = root
156 .get("occurrences")
157 .and_then(as_obj)
158 .and_then(|o| o.get("sites"))
159 .and_then(Value::as_array)
160 else {
161 return Vec::new();
162 };
163 sites
164 .iter()
165 .filter_map(|site| {
166 let obj = as_obj(site)?;
167 let file = obj.get("file").and_then(Value::as_str)?.to_string();
168 let name_line = obj
169 .get("name")
170 .and_then(as_obj)
171 .and_then(|n| n.get("line"))
172 .and_then(Value::as_u64)? as usize;
173 let resolution = obj.get("resolution").and_then(Value::as_str)?.to_string();
174 let target = obj.get("target").and_then(as_obj);
175 let target_type = target
176 .and_then(|t| t.get("type"))
177 .and_then(Value::as_str)
178 .map(str::to_string);
179 let target_member = target
180 .and_then(|t| t.get("member"))
181 .and_then(Value::as_str)
182 .map(str::to_string);
183 let compilation_identity = obj
184 .get("compilation")
185 .and_then(as_obj)
186 .and_then(|c| c.get("identity"))
187 .cloned()
188 .unwrap_or(Value::Null);
189 let target_assembly = target
190 .and_then(|t| t.get("assembly"))
191 .and_then(Value::as_str)
192 .map(str::to_string);
193 let target_generic_arity = target
194 .and_then(|t| t.get("genericArity"))
195 .and_then(Value::as_u64);
196 let target_overload_signature = target
197 .and_then(|t| t.get("overloadSignature"))
198 .and_then(Value::as_str)
199 .map(str::to_string);
200 let span = obj.get("span").and_then(as_obj);
201 let span_start_line = span
202 .and_then(|s| s.get("startLine"))
203 .and_then(Value::as_u64);
204 let span_start_char = span
205 .and_then(|s| s.get("startChar"))
206 .and_then(Value::as_u64);
207 let span_end_line = span.and_then(|s| s.get("endLine")).and_then(Value::as_u64);
208 let span_end_char = span.and_then(|s| s.get("endChar")).and_then(Value::as_u64);
209 let document_content_identity = obj
210 .get("documentContentIdentity")
211 .and_then(Value::as_str)
212 .map(str::to_string);
213 let target_document_content_identities = obj
214 .get("targetDocumentContentIdentities")
215 .and_then(Value::as_array)
216 .map(|a| {
217 a.iter()
218 .filter_map(|v| v.as_str().map(str::to_string))
219 .collect()
220 })
221 .unwrap_or_default();
222 Some(RawOccurrence {
223 file,
224 name_line,
225 resolution,
226 target_type,
227 target_member,
228 compilation_identity,
229 target_assembly,
230 target_generic_arity,
231 target_overload_signature,
232 span_start_line,
233 span_start_char,
234 span_end_line,
235 span_end_char,
236 document_content_identity,
237 target_document_content_identities,
238 })
239 })
240 .collect()
241}
242
243fn overload_arg_count(signature: &str) -> Option<usize> {
251 let after_open = signature.strip_prefix('(')?;
252 let close = after_open.find(")->")?;
253 let params = &after_open[..close];
254 if params.trim().is_empty() {
255 return Some(0);
256 }
257 let mut count = 1;
258 let mut depth = 0i32;
259 for ch in params.chars() {
260 match ch {
261 '<' | '(' | '[' => depth += 1,
262 '>' | ')' | ']' => depth -= 1,
263 ',' if depth == 0 => count += 1,
264 _ => {}
265 }
266 }
267 Some(count)
268}
269
270fn parse_raw(bytes: &[u8]) -> Option<RawFacts> {
271 let root: Value = serde_json::from_slice(bytes).ok()?;
272 let source_head_sha = root
273 .get("sourceSnapshot")
274 .and_then(as_obj)
275 .and_then(|s| s.get("headSha"))
276 .and_then(Value::as_str)
277 .map(str::to_string);
278 let source_dirty = root
281 .get("sourceSnapshot")
282 .and_then(as_obj)
283 .and_then(|s| s.get("dirty"))
284 .and_then(Value::as_bool)
285 .unwrap_or(true);
286 Some(RawFacts {
287 source_head_sha,
288 source_dirty,
289 compilations: parse_compilations(&root),
290 occurrences: parse_occurrences(&root),
291 })
292}
293
294fn split_qualified_type(raw: &str) -> Vec<String> {
300 let mut segments = Vec::new();
301 let mut current = String::new();
302 let mut depth = 0i32;
303 for ch in raw.chars() {
304 match ch {
305 '<' => {
306 depth += 1;
307 current.push(ch);
308 }
309 '>' => {
310 depth -= 1;
311 current.push(ch);
312 }
313 '.' if depth == 0 => {
314 segments.push(current.clone());
315 current.clear();
316 }
317 _ => current.push(ch),
318 }
319 }
320 if !current.is_empty() {
321 segments.push(current);
322 }
323 segments
324 .into_iter()
325 .map(|seg| match seg.find('<') {
326 Some(i) => seg[..i].to_string(),
327 None => seg,
328 })
329 .collect()
330}
331
332fn translate_type_id(raw: &str, index: &DefIndex) -> Option<usize> {
341 let segments = split_qualified_type(raw);
342 for k in 1..=segments.len() {
343 let head = segments[..k].join(".");
344 let Some(&start) = index.qualified_name_to_def.get(&head) else {
345 continue;
346 };
347 let mut cur = start;
348 let mut complete = true;
349 for seg in &segments[k..] {
350 let candidate = format!("{}+{}", index.defs[cur].id, seg);
351 match index.qualified_name_to_def.get(&candidate) {
352 Some(&next) => cur = next,
353 None => {
354 complete = false;
355 break;
356 }
357 }
358 }
359 if complete {
360 return Some(cur);
361 }
362 }
363 None
364}
365
366#[derive(Debug, Clone, PartialEq, Eq)]
379pub struct SemanticTarget {
380 pub to_def_id: String,
382 pub to_file: String,
384 pub overload_signature: Option<String>,
386}
387
388fn resolve_target(
389 type_idx: usize,
390 member: &str,
391 overload_signature: Option<&str>,
392 index: &DefIndex,
393) -> SemanticTarget {
394 let def = &index.defs[type_idx];
395 let overload_signature = overload_signature.map(str::to_string);
396 if def.kind == "enum" {
397 let key = format!("{}.{member}", def.id);
398 if let Some(&member_idx) = index.qualified_name_to_def.get(&key) {
399 let member_def = &index.defs[member_idx];
400 return SemanticTarget {
401 to_def_id: member_def.id.clone(),
402 to_file: member_def.file.clone(),
403 overload_signature,
404 };
405 }
406 }
407 SemanticTarget {
408 to_def_id: def.id.clone(),
409 to_file: def.file.clone(),
410 overload_signature,
411 }
412}
413
414#[derive(Debug, Clone, PartialEq, Eq)]
417pub enum LookupOutcome {
418 NoFact,
421 Confirmed(SemanticTarget),
424 Other(Uncertainty),
427 ConfirmedMany(Vec<SemanticTarget>),
439}
440
441pub struct SemanticLayer {
446 freshness: Freshness,
447 by_site: HashMap<(String, usize, String), Vec<usize>>,
448 occurrences: Vec<RawOccurrence>,
449 compilations: Vec<RawCompilation>,
450}
451
452impl SemanticLayer {
453 pub fn load(root: &Path) -> Option<SemanticLayer> {
459 let admitted = read_compiler_facts(root)?;
460 let raw = parse_raw(&admitted.bytes)?;
461 let imports: Vec<freshness::ImportRef> = raw
462 .compilations
463 .iter()
464 .flat_map(|c| c.imports.iter().cloned())
465 .collect();
466 let freshness = freshness::evaluate(
467 root,
468 raw.source_head_sha.as_deref(),
469 raw.source_dirty,
470 &imports,
471 );
472
473 let mut by_site: HashMap<(String, usize, String), Vec<usize>> = HashMap::new();
474 for (i, occ) in raw.occurrences.iter().enumerate() {
475 if let Some(member) = &occ.target_member {
476 by_site
477 .entry((occ.file.clone(), occ.name_line, member.clone()))
478 .or_default()
479 .push(i);
480 }
481 }
482
483 Some(SemanticLayer {
484 freshness,
485 by_site,
486 occurrences: raw.occurrences,
487 compilations: raw.compilations,
488 })
489 }
490
491 pub fn is_fresh(&self) -> bool {
494 self.freshness.is_fresh()
495 }
496
497 pub fn site_keys(&self) -> impl Iterator<Item = &(String, usize, String)> {
501 self.by_site.keys()
502 }
503
504 fn compilation_state(&self, identity: &Value) -> Option<(&str, &str)> {
505 self.compilations
506 .iter()
507 .find(|c| &c.identity == identity)
508 .map(|c| (c.state.as_str(), c.reason.as_str()))
509 }
510
511 pub fn lookup(
530 &self,
531 index: &DefIndex,
532 file: &str,
533 line: usize,
534 member: &str,
535 caller_arg_count: Option<usize>,
536 ) -> LookupOutcome {
537 if !self.freshness.is_fresh() {
538 let Freshness::Stale { reason } = &self.freshness else {
539 unreachable!("is_fresh() already returned false");
540 };
541 return LookupOutcome::Other(Uncertainty::Stale {
542 reason: reason.clone(),
543 });
544 }
545 let Some(indices) = self
546 .by_site
547 .get(&(file.to_string(), line, member.to_string()))
548 else {
549 return LookupOutcome::NoFact;
550 };
551
552 let narrowed: Vec<usize>;
555 let indices: &[usize] = if indices.len() > 1 {
556 if let Some(want) = caller_arg_count {
557 let matching: Vec<usize> = indices
558 .iter()
559 .copied()
560 .filter(|&i| {
561 self.occurrences[i]
562 .target_overload_signature
563 .as_deref()
564 .and_then(overload_arg_count)
565 == Some(want)
566 })
567 .collect();
568 if matching.len() == 1 {
569 narrowed = matching;
570 &narrowed
571 } else {
572 indices.as_slice()
573 }
574 } else {
575 indices.as_slice()
576 }
577 } else {
578 indices.as_slice()
579 };
580
581 let mut resolved_targets: Vec<SemanticTarget> = Vec::new();
582 let mut other: Option<Uncertainty> = None;
583 for &i in indices {
584 let occ = &self.occurrences[i];
585 let state = self.compilation_state(&occ.compilation_identity);
586 let uncertainty = match state {
587 Some((cstate, creason)) => uncertainty::from_compilation_state(cstate, creason)
588 .unwrap_or_else(|| uncertainty::from_resolution(&occ.resolution)),
589 None => uncertainty::from_resolution(&occ.resolution),
590 };
591 if uncertainty.is_confirmed() {
592 if let Some(type_id) = occ.target_type.as_deref() {
593 if let Some(type_idx) = translate_type_id(type_id, index) {
594 resolved_targets.push(resolve_target(
595 type_idx,
596 member,
597 occ.target_overload_signature.as_deref(),
598 index,
599 ));
600 continue;
601 }
602 }
603 other.get_or_insert(Uncertainty::Candidate);
608 } else {
609 other.get_or_insert(uncertainty);
610 }
611 }
612
613 resolved_targets.dedup();
614 match resolved_targets.len() {
615 0 => other
616 .map(LookupOutcome::Other)
617 .unwrap_or(LookupOutcome::NoFact),
618 1 => LookupOutcome::Confirmed(resolved_targets.remove(0)),
619 _ => LookupOutcome::ConfirmedMany(resolved_targets),
620 }
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627
628 #[test]
629 fn overload_arg_count_reads_a_zero_arg_signature() {
630 assert_eq!(overload_arg_count("()->void"), Some(0));
631 }
632
633 #[test]
634 fn overload_arg_count_counts_top_level_commas() {
635 assert_eq!(overload_arg_count("(bool, string)->void"), Some(2));
636 }
637
638 #[test]
639 fn overload_arg_count_never_splits_inside_a_generic_argument_list() {
640 assert_eq!(
641 overload_arg_count("(System.Collections.Generic.Dictionary<string, int>)->void"),
642 Some(1)
643 );
644 }
645
646 #[test]
647 fn overload_arg_count_is_none_for_a_string_with_no_parenthesized_shape() {
648 assert_eq!(overload_arg_count("void"), None);
649 assert_eq!(overload_arg_count(""), None);
650 }
651
652 #[test]
653 fn splits_plain_dotted_segments() {
654 assert_eq!(
655 split_qualified_type("Ns.Outer.Widget"),
656 vec!["Ns", "Outer", "Widget"]
657 );
658 }
659
660 #[test]
661 fn strips_a_trailing_generic_suffix_per_segment() {
662 assert_eq!(
663 split_qualified_type("Ns.Outer.Nested<T>"),
664 vec!["Ns", "Outer", "Nested"]
665 );
666 }
667
668 #[test]
669 fn never_splits_inside_a_generic_argument_list() {
670 assert_eq!(
671 split_qualified_type("Ns.Holder<System.String>.Leaf"),
672 vec!["Ns", "Holder", "Leaf"]
673 );
674 }
675
676 #[test]
677 fn a_doubly_nested_generic_strips_every_level() {
678 assert_eq!(
679 split_qualified_type("Ns.Outer<K>.Middle<V>.Leaf"),
680 vec!["Ns", "Outer", "Middle", "Leaf"]
681 );
682 }
683}