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
90#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
91pub enum CallSyntax {
92 Path(Vec<String>),
93 SelfMethod(String),
94 Method { receiver: String, method: String },
95}
96
97impl CallSyntax {
98 pub fn key_fragment(&self) -> String {
99 match self {
100 Self::Path(parts) => parts.join("::"),
101 Self::SelfMethod(method) => format!("self.{method}"),
102 Self::Method { receiver, method } => format!("{receiver}.{method}"),
103 }
104 }
105}
106
107#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
108pub struct CallSite {
109 pub id: CallSiteId,
114 pub syntax: CallSyntax,
115 pub target: CallTarget,
119 pub label: CallLabel,
122 pub span: SourceSpan,
123}
124
125#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
126pub enum CallTarget {
127 #[default]
128 Unresolved,
129 Indirect {
132 signature: Option<String>,
133 reason: UnresolvedReason,
134 },
135 Direct(SymbolId),
136 Dynamic {
137 dispatch: SymbolId,
138 candidates: Vec<DispatchCandidate>,
139 resolution: DispatchResolution,
140 evidence: DispatchEvidence,
141 unresolved_reasons: BTreeSet<UnresolvedReason>,
142 },
143}
144
145#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
147pub enum DispatchEvidence {
148 #[default]
150 ExactFlow,
151 ClosedSet,
153}
154
155impl fmt::Display for DispatchEvidence {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 f.write_str(match self {
158 Self::ExactFlow => "exact-flow",
159 Self::ClosedSet => "closed-set",
160 })
161 }
162}
163
164#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
166pub enum UnresolvedReason {
167 OpaqueInput,
168 ExternalCode,
169 ExternalMemory,
170 FunctionPointer,
171 CrossCrateBoundary,
172 AnalysisLimit,
173 UnsupportedConstruct,
174}
175
176impl fmt::Display for UnresolvedReason {
177 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178 let text = match self {
179 Self::OpaqueInput => "opaque input",
180 Self::ExternalCode => "external code",
181 Self::ExternalMemory => "external memory",
182 Self::FunctionPointer => "function pointer",
183 Self::CrossCrateBoundary => "cross-crate boundary",
184 Self::AnalysisLimit => "analysis limit",
185 Self::UnsupportedConstruct => "unsupported construct",
186 };
187 f.write_str(text)
188 }
189}
190
191#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
197pub enum DispatchResolution {
198 #[default]
199 Complete,
200 Partial,
201 Unresolved,
202}
203
204#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
205pub struct DispatchCandidate {
206 pub target: SymbolId,
207 pub label: CallLabel,
208}
209
210#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
211pub struct CallLabel {
212 pub default: String,
213 pub typed: Option<String>,
214}
215
216impl CallLabel {
217 pub fn new(default: impl Into<String>) -> Self {
218 Self {
219 default: default.into(),
220 typed: None,
221 }
222 }
223
224 pub fn with_types(default: impl Into<String>, typed: impl Into<String>) -> Self {
225 Self {
226 default: default.into(),
227 typed: Some(typed.into()),
228 }
229 }
230
231 pub fn text(&self, show_types: bool) -> &str {
232 if show_types {
233 self.typed.as_deref().unwrap_or(&self.default)
234 } else {
235 &self.default
236 }
237 }
238
239 pub fn with_suffix(&self, suffix: &str) -> Self {
240 Self {
241 default: format!("{}{suffix}", self.default),
242 typed: self.typed.as_ref().map(|typed| format!("{typed}{suffix}")),
243 }
244 }
245}
246
247#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
248pub struct FunctionInfo {
249 pub id: SymbolId,
250 pub label: CallLabel,
252 pub public: bool,
253 pub calls: Vec<CallSite>,
254 pub span: SourceSpan,
255}
256
257#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
258pub struct LanguageFact {
259 pub subject: SymbolId,
260 pub namespace: LanguageId,
261 pub kind: String,
262 pub key: String,
263 pub value: String,
264 pub span: SourceSpan,
265}
266
267#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
268pub struct SemanticCallGraph {
269 pub functions: Vec<FunctionInfo>,
270 pub facts: Vec<LanguageFact>,
271 pub source_files: BTreeSet<PathBuf>,
274 pub roots: BTreeSet<SymbolId>,
277}
278
279pub type FileAnalysis = SemanticCallGraph;
282
283#[derive(Clone, Debug, Eq, PartialEq)]
284pub struct CallNode {
285 pub key: String,
286 pub callsite: Option<CallSiteId>,
288 pub label: CallLabel,
289 pub relation: CallRelation,
291 pub children: Vec<CallNode>,
292}
293
294#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
295pub enum CallRelation {
296 #[default]
297 Call,
298 DispatchCandidate,
299 BackEdge,
303}