1use std::collections::HashSet;
11
12use crate::graph::{
13 Edge, EdgesByKind, FragRef, HeuristicByTier, HeuristicTier, SemanticProvenance,
14};
15use crate::resolve::DefIndex;
16
17use super::discovered;
18use super::layer::{LookupOutcome, SemanticLayer, SemanticTarget};
19use crate::graph::SemanticStats;
20
21#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct Override {
27 pub target: SemanticTarget,
29 pub displaced: Option<String>,
31}
32
33pub fn decide(outcome: LookupOutcome, ladder_pushed: &[Edge]) -> Option<Override> {
38 let LookupOutcome::Confirmed(target) = outcome else {
39 return None;
40 };
41 let displaced = ladder_pushed.iter().find_map(|e| match e {
42 Edge::UsesMember { to, .. } if *to != target.to_def_id => Some(to.clone()),
43 _ => None,
44 });
45 Some(Override { target, displaced })
46}
47
48#[derive(Debug, Clone, Copy, Default)]
53pub struct SemanticDiagnostics {
54 pub confirmed: usize,
56 pub disagreements: usize,
58}
59
60impl SemanticDiagnostics {
61 pub fn record(&mut self, ovr: &Override) {
64 self.confirmed += 1;
65 if ovr.displaced.is_some() {
66 self.disagreements += 1;
67 }
68 }
69}
70
71pub fn track_reference(
77 layer: Option<&SemanticLayer>,
78 sites: &mut HashSet<(String, usize, String)>,
79 file: &str,
80 r: &FragRef,
81) {
82 if layer.is_some() {
83 if let Some(member) = &r.member {
84 sites.insert((file.to_string(), r.line, member.clone()));
85 }
86 }
87}
88
89#[allow(
97 clippy::too_many_arguments,
98 reason = "each parameter is one of the resolver's own ledger fields, individually mutable, and the design forbids a post-pass over the finished edge vector that could fold them into one struct"
99)]
100pub fn apply(
101 layer: Option<&SemanticLayer>,
102 index: &DefIndex,
103 edges: &mut Vec<Edge>,
104 edges_by_kind: &mut EdgesByKind,
105 heuristic_edge_count: &mut usize,
106 heuristic_by_tier: &mut HeuristicByTier,
107 diag: &mut SemanticDiagnostics,
108 pre_len: usize,
109 file: &str,
110 r: &FragRef,
111) {
112 let (Some(layer), Some(member)) = (layer, r.member.as_deref()) else {
113 return;
114 };
115 let outcome = layer.lookup(index, file, r.line, member, r.arg_count);
116 let Some(ovr) = decide(outcome, &edges[pre_len..]) else {
117 return;
118 };
119 diag.record(&ovr);
120 let mut precise_truncated = 0usize;
131 for e in &edges[pre_len..] {
132 match e {
133 Edge::UsesMember {
134 tier: Some(tier), ..
135 } => {
136 *heuristic_edge_count -= 1;
137 match tier {
138 HeuristicTier::Ext => heuristic_by_tier.ext -= 1,
139 HeuristicTier::Guess => heuristic_by_tier.guess -= 1,
140 }
141 }
142 Edge::UsesMember { tier: None, .. } => precise_truncated += 1,
143 _ => {}
144 }
145 }
146 edges_by_kind.uses_member -= precise_truncated;
147 edges.truncate(pre_len);
148 edges_by_kind.uses_member += 1;
149 edges.push(Edge::uses_member_semantic(
150 file.to_string(),
151 r.line,
152 ovr.target.to_def_id,
153 ovr.target.to_file,
154 Some(member.to_string()),
155 SemanticProvenance::Semantic,
156 ovr.target.overload_signature,
157 ));
158}
159
160pub fn finish(
165 layer: Option<&SemanticLayer>,
166 index: &DefIndex,
167 sites: &HashSet<(String, usize, String)>,
168 edges: &mut Vec<Edge>,
169 diag: SemanticDiagnostics,
170) -> Option<SemanticStats> {
171 let layer = layer?;
172 let discovered = discovered::project(layer, index, sites, edges);
173 Some(SemanticStats {
174 confirmed: diag.confirmed,
175 disagreements: diag.disagreements,
176 discovered,
177 })
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::graph::HeuristicTier;
184
185 fn target(id: &str) -> SemanticTarget {
186 SemanticTarget {
187 to_def_id: id.to_string(),
188 to_file: "Target.cs".to_string(),
189 overload_signature: None,
190 }
191 }
192
193 #[test]
194 fn a_non_confirmed_outcome_never_overrides() {
195 assert!(decide(LookupOutcome::NoFact, &[]).is_none());
196 }
197
198 #[test]
199 fn confirming_the_same_target_the_ladder_already_bound_has_no_displaced_target() {
200 let pushed = vec![Edge::uses_member(
201 "Caller.cs".to_string(),
202 8,
203 "Ns.Widget".to_string(),
204 "Widget.cs".to_string(),
205 Some("Render".to_string()),
206 None,
207 )];
208 let ovr = decide(LookupOutcome::Confirmed(target("Ns.Widget")), &pushed).unwrap();
209 assert_eq!(ovr.displaced, None);
210 }
211
212 #[test]
213 fn confirming_a_different_target_records_the_displaced_one() {
214 let pushed = vec![Edge::uses_member(
215 "Caller.cs".to_string(),
216 8,
217 "Ns.WrongGuess".to_string(),
218 "WrongGuess.cs".to_string(),
219 Some("Render".to_string()),
220 Some(HeuristicTier::Guess),
221 )];
222 let ovr = decide(LookupOutcome::Confirmed(target("Ns.Widget")), &pushed).unwrap();
223 assert_eq!(ovr.displaced.as_deref(), Some("Ns.WrongGuess"));
224 }
225
226 #[test]
227 fn diagnostics_count_confirmations_and_disagreements_separately() {
228 let mut diag = SemanticDiagnostics::default();
229 diag.record(&Override {
230 target: target("Ns.Widget"),
231 displaced: None,
232 });
233 diag.record(&Override {
234 target: target("Ns.Widget"),
235 displaced: Some("Ns.WrongGuess".to_string()),
236 });
237 assert_eq!(diag.confirmed, 2);
238 assert_eq!(diag.disagreements, 1);
239 }
240}