1use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
18#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
19pub enum RecvKind {
21 None,
22 This,
23 #[serde(rename = "SELF")]
24 SelfRecv,
25 NamedVariable,
26 TypeQualified,
27 FieldOfThis,
28 FieldOfSelf,
29 FieldOfVariable,
30 Super,
31 StaticType,
32 #[default]
33 Unknown,
34}
35
36impl RecvKind {
38 pub fn as_str(self) -> &'static str {
40 match self {
41 RecvKind::None => "NONE",
42 RecvKind::This => "THIS",
43 RecvKind::SelfRecv => "SELF",
44 RecvKind::NamedVariable => "NAMED_VARIABLE",
45 RecvKind::TypeQualified => "TYPE_QUALIFIED",
46 RecvKind::FieldOfThis => "FIELD_OF_THIS",
47 RecvKind::FieldOfSelf => "FIELD_OF_SELF",
48 RecvKind::FieldOfVariable => "FIELD_OF_VARIABLE",
49 RecvKind::Super => "SUPER",
50 RecvKind::StaticType => "STATIC_TYPE",
51 RecvKind::Unknown => "UNKNOWN_RECEIVER",
52 }
53 }
54
55 pub fn is_instance_self(self) -> bool {
58 matches!(self, RecvKind::This | RecvKind::SelfRecv)
59 }
60
61 pub fn is_field_chain(self) -> bool {
67 matches!(
68 self,
69 RecvKind::FieldOfThis | RecvKind::FieldOfSelf | RecvKind::FieldOfVariable
70 )
71 }
72}
73
74#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
80#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
81pub enum ResolutionClass {
83 ResolvedInternal,
84 UnresolvedLikelyInternal,
85 ConfirmedExternal,
86 #[default]
87 Unknown,
88}
89
90impl ResolutionClass {
92 pub fn as_str(self) -> &'static str {
94 match self {
95 ResolutionClass::ResolvedInternal => "resolved_internal",
96 ResolutionClass::UnresolvedLikelyInternal => "unresolved_likely_internal",
97 ResolutionClass::ConfirmedExternal => "confirmed_external",
98 ResolutionClass::Unknown => "unknown",
99 }
100 }
101}
102
103pub mod confidence {
112 pub const LSP_EXACT: f64 = 0.99;
114 pub const SCIP_EXACT: f64 = 0.99;
116 pub const UNIQUE_PIN: f64 = 0.99;
121 pub const SIBLING_METHOD: f64 = 0.98;
123 pub const NAMESPACE_PIN: f64 = 0.97;
125 pub const IMPORTED_MEMBER: f64 = 0.95;
127 pub const TYPED_RECEIVER: f64 = 0.9;
131 pub const CONFIRMED_EXTERNAL: f64 = 0.8;
133 pub const STRONG_LINK: f64 = 0.8;
136 pub const SEEDED_ROOT: f64 = 0.55;
138 pub const UNRESOLVED_FALLBACK: f64 = 0.5;
140 pub const UNRESOLVED_LIKELY_INTERNAL: f64 = 0.4;
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema)]
145pub struct AnalysisQuality {
147 pub calls: CallQuality,
148 pub files: FileQuality,
149 #[serde(default, skip_serializing_if = "is_zero")]
150 pub matched_doc_mentions: u32,
151 #[serde(default, skip_serializing_if = "is_zero")]
152 pub unmatched_doc_mentions: u32,
153}
154
155fn is_zero(n: &u32) -> bool {
157 *n == 0
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema)]
161pub struct CallQuality {
163 pub resolved: u32,
164 #[serde(default, skip_serializing_if = "is_zero")]
165 pub precise: u32,
166 #[serde(default, skip_serializing_if = "is_zero")]
167 pub heuristic: u32,
168 pub likely_internal_unresolved: u32,
169 pub external: u32,
170 #[serde(default, skip_serializing_if = "is_zero")]
171 pub unknown: u32,
172}
173
174#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize, schemars::JsonSchema)]
175pub struct FileQuality {
177 pub parsed: u32,
178 #[serde(default, skip_serializing_if = "is_zero")]
179 pub unsupported: u32,
180}
181
182impl AnalysisQuality {
184pub fn record_call(&mut self, class: ResolutionClass, precise: bool) {
186 match class {
187 ResolutionClass::ResolvedInternal => {
188 self.calls.resolved += 1;
189 if precise {
190 self.calls.precise += 1;
191 } else {
192 self.calls.heuristic += 1;
193 }
194 }
195 ResolutionClass::UnresolvedLikelyInternal => {
196 self.calls.likely_internal_unresolved += 1
197 }
198 ResolutionClass::ConfirmedExternal => self.calls.external += 1,
199 ResolutionClass::Unknown => self.calls.unknown += 1,
200 }
201 }
202
203 pub fn merge(&mut self, other: &AnalysisQuality) {
206 self.calls.resolved += other.calls.resolved;
207 self.calls.precise += other.calls.precise;
208 self.calls.heuristic += other.calls.heuristic;
209 self.calls.likely_internal_unresolved += other.calls.likely_internal_unresolved;
210 self.calls.external += other.calls.external;
211 self.calls.unknown += other.calls.unknown;
212 self.files.parsed += other.files.parsed;
213 self.files.unsupported += other.files.unsupported;
214 self.matched_doc_mentions += other.matched_doc_mentions;
215 self.unmatched_doc_mentions += other.unmatched_doc_mentions;
216 }
217
218 pub fn compact_line(&self) -> String {
220 let mut line = format!(
221 "calls: resolved={} precise={} heuristic={} likely_internal_unresolved={} external={} | files: parsed={} unsupported={}",
222 self.calls.resolved,
223 self.calls.precise,
224 self.calls.heuristic,
225 self.calls.likely_internal_unresolved,
226 self.calls.external,
227 self.files.parsed,
228 self.files.unsupported,
229 );
230 if self.matched_doc_mentions > 0 || self.unmatched_doc_mentions > 0 {
231 line.push_str(&format!(
232 " | mentions: matched={} unmatched={}",
233 self.matched_doc_mentions, self.unmatched_doc_mentions
234 ));
235 }
236 line
237 }
238}
239
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
243#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
244pub enum RepresentationKind {
246 Exact,
247 Structural,
248 Signatures,
249}
250
251#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
252pub struct RepresentationChoice {
254 pub kind: RepresentationKind,
255 pub reason: String,
256 pub exact_cost: usize,
257 pub structural_cost: usize,
258}
259
260pub fn choose_representation(exact_cost: usize, structural_cost: usize) -> RepresentationChoice {
263 if exact_cost <= structural_cost {
264 RepresentationChoice {
265 kind: RepresentationKind::Exact,
266 reason: "exact_body_cheaper_than_structural".into(),
267 exact_cost,
268 structural_cost,
269 }
270 } else {
271 let saved = structural_cost
272 .saturating_mul(100)
273 .checked_div(exact_cost)
274 .map(|pct| 100usize.saturating_sub(pct))
275 .unwrap_or(0);
276 RepresentationChoice {
277 kind: RepresentationKind::Structural,
278 reason: format!("structural_saved_{saved}_percent"),
279 exact_cost,
280 structural_cost,
281 }
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288
289 #[test]
290 fn exact_wins_when_not_more_expensive() {
292 let c = choose_representation(40, 40);
293 assert_eq!(c.kind, RepresentationKind::Exact);
294 assert_eq!(c.reason, "exact_body_cheaper_than_structural");
295 let cheaper = choose_representation(10, 80);
296 assert_eq!(cheaper.kind, RepresentationKind::Exact);
297 }
298
299 #[test]
300 fn structural_wins_when_it_saves_tokens() {
302 let c = choose_representation(100, 29);
303 assert_eq!(c.kind, RepresentationKind::Structural);
304 assert!(c.reason.contains("structural_saved_"), "{}", c.reason);
305 }
306
307 #[test]
308 fn record_call_buckets_are_honest() {
310 let mut q = AnalysisQuality::default();
311 q.record_call(ResolutionClass::ResolvedInternal, true);
312 q.record_call(ResolutionClass::ResolvedInternal, false);
313 q.record_call(ResolutionClass::ConfirmedExternal, false);
314 q.record_call(ResolutionClass::UnresolvedLikelyInternal, false);
315 q.record_call(ResolutionClass::Unknown, false);
316 assert_eq!(q.calls.resolved, 2);
317 assert_eq!(q.calls.precise, 1);
318 assert_eq!(q.calls.heuristic, 1);
319 assert_eq!(q.calls.external, 1);
320 assert_eq!(q.calls.likely_internal_unresolved, 1);
321 assert_eq!(q.calls.unknown, 1);
322 assert!(q.compact_line().contains("precise=1"));
323 assert!(q.compact_line().contains("external=1"));
324 }
325}