1use std::collections::BTreeSet;
2use std::fmt;
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7pub type NodeId = SymbolId;
13
14#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
15pub struct CallSiteId(pub String);
16
17impl CallSiteId {
18 pub fn source(syntax: &CallSyntax, span: &SourceSpan) -> Self {
19 Self(format!(
20 "{}@{}:{}-{}:{}",
21 syntax.key_fragment(),
22 span.start_line,
23 span.start_column,
24 span.end_line,
25 span.end_column
26 ))
27 }
28}
29
30#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
31pub struct LanguageId(pub String);
32
33impl LanguageId {
34 pub fn new(value: impl Into<String>) -> Self {
35 Self(value.into())
36 }
37}
38
39impl fmt::Display for LanguageId {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 self.0.fmt(f)
42 }
43}
44
45#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
46pub struct SymbolId {
47 pub language: LanguageId,
48 pub module: Vec<String>,
49 pub container: Option<String>,
50 pub name: String,
51}
52
53impl SymbolId {
54 pub fn short_name(&self) -> String {
55 match &self.container {
56 Some(container) => format!("{container}::{}", self.name),
57 None => self.name.clone(),
58 }
59 }
60
61 pub fn qualified_parts(&self) -> Vec<&str> {
62 self.module
63 .iter()
64 .map(String::as_str)
65 .chain(self.container.iter().map(String::as_str))
66 .chain(std::iter::once(self.name.as_str()))
67 .collect()
68 }
69}
70
71impl fmt::Display for SymbolId {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(f, "{}://", self.language)?;
74 let parts = self.qualified_parts();
75 write!(f, "{}", parts.join("::"))
76 }
77}
78
79#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
80pub struct SourceSpan {
81 pub file: PathBuf,
82 pub start_line: usize,
83 pub start_column: usize,
84 pub start_byte: Option<usize>,
85 pub end_line: usize,
86 pub end_column: usize,
87 pub end_byte: Option<usize>,
88}
89
90impl SourceSpan {
91 pub fn has_same_coordinates(&self, other: &Self) -> bool {
92 self.start_line == other.start_line
93 && self.start_column == other.start_column
94 && self.end_line == other.end_line
95 && self.end_column == other.end_column
96 }
97
98 pub fn contains(&self, other: &Self) -> bool {
99 (self.start_line, self.start_column) <= (other.start_line, other.start_column)
100 && (other.end_line, other.end_column) <= (self.end_line, self.end_column)
101 }
102
103 pub fn overlaps(&self, other: &Self) -> bool {
104 (self.start_line, self.start_column) <= (other.end_line, other.end_column)
105 && (other.start_line, other.start_column) <= (self.end_line, self.end_column)
106 }
107
108 pub fn extent(&self) -> usize {
110 self.end_line.saturating_sub(self.start_line) * 10_000
111 + self.end_column.saturating_sub(self.start_column)
112 }
113
114 pub fn start_distance(&self, other: &Self) -> usize {
115 self.start_line.abs_diff(other.start_line) * 10_000
116 + self.start_column.abs_diff(other.start_column)
117 }
118
119 pub fn boundary_distance(&self, other: &Self) -> usize {
120 self.start_distance(other)
121 + self.end_line.abs_diff(other.end_line) * 10_000
122 + self.end_column.abs_diff(other.end_column)
123 }
124}
125
126#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
127pub enum CallSyntax {
128 Path(Vec<String>),
129 SelfMethod(String),
130 Method {
131 receiver: String,
132 method: String,
133 },
134 Expression(String),
139 CompilerConfirmed(Box<CallSyntax>),
143}
144
145impl CallSyntax {
146 pub fn key_fragment(&self) -> String {
147 match self {
148 Self::Path(parts) => parts.join("::"),
149 Self::SelfMethod(method) => format!("self.{method}"),
150 Self::Method { receiver, method } => format!("{receiver}.{method}"),
151 Self::Expression(expression) => expression.clone(),
152 Self::CompilerConfirmed(syntax) => {
153 format!("compiler-confirmed:{}", syntax.key_fragment())
154 }
155 }
156 }
157
158 pub fn visible(&self) -> &Self {
159 match self {
160 Self::CompilerConfirmed(syntax) => syntax.visible(),
161 syntax => syntax,
162 }
163 }
164
165 pub fn requires_compiler_confirmation(&self) -> bool {
166 matches!(self, Self::CompilerConfirmed(_))
167 }
168}
169
170#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
171pub struct CallSite {
172 pub id: CallSiteId,
177 pub syntax: CallSyntax,
178 pub target: CallTarget,
182 pub label: CallLabel,
185 pub span: SourceSpan,
186}
187
188#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
189pub enum CallTarget {
190 #[default]
191 Unresolved,
192 Indirect {
195 signature: Option<String>,
196 reason: UnresolvedReason,
197 },
198 Direct(SymbolId),
199 Dynamic {
200 dispatch: SymbolId,
201 candidates: Vec<DispatchCandidate>,
202 resolution: DispatchResolution,
203 evidence: DispatchEvidence,
204 unresolved_reasons: BTreeSet<UnresolvedReason>,
205 },
206}
207
208impl CallTarget {
209 pub fn dynamic(
210 dispatch: SymbolId,
211 candidates: Vec<DispatchCandidate>,
212 evidence: DispatchEvidence,
213 unresolved_reasons: BTreeSet<UnresolvedReason>,
214 ) -> Self {
215 let resolution = DispatchResolution::from_candidates(
216 !candidates.is_empty(),
217 !unresolved_reasons.is_empty(),
218 );
219 Self::Dynamic {
220 dispatch,
221 candidates,
222 resolution,
223 evidence,
224 unresolved_reasons,
225 }
226 }
227}
228
229#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
231pub enum DispatchEvidence {
232 #[default]
234 ExactFlow,
235 ClosedSet,
237}
238
239impl fmt::Display for DispatchEvidence {
240 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241 f.write_str(match self {
242 Self::ExactFlow => "exact-flow",
243 Self::ClosedSet => "closed-set",
244 })
245 }
246}
247
248#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
250pub enum UnresolvedReason {
251 OpaqueInput,
252 ExternalCode,
253 ExternalMemory,
254 FunctionPointer,
255 CrossCrateBoundary,
256 AnalysisLimit,
257 UnsupportedConstruct,
258}
259
260impl fmt::Display for UnresolvedReason {
261 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262 let text = match self {
263 Self::OpaqueInput => "opaque input",
264 Self::ExternalCode => "external code",
265 Self::ExternalMemory => "external memory",
266 Self::FunctionPointer => "function pointer",
267 Self::CrossCrateBoundary => "cross-crate boundary",
268 Self::AnalysisLimit => "analysis limit",
269 Self::UnsupportedConstruct => "unsupported construct",
270 };
271 f.write_str(text)
272 }
273}
274
275#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
281pub enum DispatchResolution {
282 #[default]
283 Complete,
284 Partial,
285 Unresolved,
286}
287
288impl DispatchResolution {
289 pub fn from_candidates(has_candidates: bool, has_unresolved_sources: bool) -> Self {
290 match (has_candidates, has_unresolved_sources) {
291 (false, _) => Self::Unresolved,
292 (true, true) => Self::Partial,
293 (true, false) => Self::Complete,
294 }
295 }
296}
297
298#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
299pub struct DispatchCandidate {
300 pub target: SymbolId,
301 pub label: CallLabel,
302}
303
304#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
305pub struct CallLabel {
306 pub default: String,
307 pub typed: Option<String>,
308}
309
310impl CallLabel {
311 pub fn new(default: impl Into<String>) -> Self {
312 Self {
313 default: default.into(),
314 typed: None,
315 }
316 }
317
318 pub fn with_types(default: impl Into<String>, typed: impl Into<String>) -> Self {
319 Self {
320 default: default.into(),
321 typed: Some(typed.into()),
322 }
323 }
324
325 pub fn text(&self, show_types: bool) -> &str {
326 if show_types {
327 self.typed.as_deref().unwrap_or(&self.default)
328 } else {
329 &self.default
330 }
331 }
332
333 pub fn with_suffix(&self, suffix: &str) -> Self {
334 Self {
335 default: format!("{}{suffix}", self.default),
336 typed: self.typed.as_ref().map(|typed| format!("{typed}{suffix}")),
337 }
338 }
339}
340
341#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
342pub struct FunctionInfo {
343 pub id: SymbolId,
344 pub label: CallLabel,
346 pub public: bool,
347 pub calls: Vec<CallSite>,
348 pub span: SourceSpan,
349}
350
351#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
352pub struct LanguageFact {
353 pub subject: SymbolId,
354 pub namespace: LanguageId,
355 pub kind: String,
356 pub key: String,
357 pub value: String,
358 pub span: SourceSpan,
359}
360
361#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
362pub struct SemanticCallGraph {
363 pub functions: Vec<FunctionInfo>,
364 pub facts: Vec<LanguageFact>,
365 pub source_files: BTreeSet<PathBuf>,
368 pub roots: BTreeSet<SymbolId>,
371}
372
373impl SemanticCallGraph {
374 pub fn append(&mut self, mut fragment: Self) {
378 self.functions.append(&mut fragment.functions);
379 self.facts.append(&mut fragment.facts);
380 self.source_files.append(&mut fragment.source_files);
381 self.roots.append(&mut fragment.roots);
382 }
383}
384
385pub type FileAnalysis = SemanticCallGraph;
388
389#[derive(Clone, Debug, Eq, PartialEq)]
390pub struct CallNode {
391 pub key: String,
392 pub callsite: Option<CallSiteId>,
394 pub label: CallLabel,
395 pub relation: CallRelation,
397 pub children: Vec<CallNode>,
398}
399
400#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
401pub enum CallRelation {
402 #[default]
403 Call,
404 DispatchCandidate,
405 BackEdge,
409}