Skip to main content

diffkit/
model.rs

1use std::collections::BTreeSet;
2use std::fmt;
3use std::path::PathBuf;
4
5use serde::{Deserialize, Serialize};
6
7/// Stable identity of a semantic definition inside one analyzed program.
8///
9/// `SymbolId` is the public, language-neutral representation of a definition;
10/// this alias makes the distinction between definition and edge identities
11/// explicit in the graph/diff layers without duplicating the symbol payload.
12pub 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    /// Identity of this source call edge. Backends derive it from the owning
110    /// definition and the call's structural source identity. It is kept
111    /// separate from the callee because one function may call the same target
112    /// more than once.
113    pub id: CallSiteId,
114    pub syntax: CallSyntax,
115    /// Authoritative semantic target supplied by rustc_public, OCaml Typedtree, or
116    /// another language backend. Syntax-only frontends use `Unresolved` and
117    /// the common graph falls back to conservative path resolution.
118    pub target: CallTarget,
119    /// Fully formatted by the owning language frontend. Core rendering never
120    /// needs to know whether a call uses `f(x)` or `f x` syntax.
121    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    /// A typed indirect call whose concrete callable value is not known.
130    /// This is intentionally distinct from a syntax-resolution failure.
131    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/// Why a candidate set is justified when it is complete.
146#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
147pub enum DispatchEvidence {
148    /// Concrete values were followed to this exact call site.
149    #[default]
150    ExactFlow,
151    /// The backend proved a closed candidate universe for this call site.
152    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/// Machine-readable causes for an unresolved or partially resolved call.
165#[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/// How completely a language backend resolved an indirect call site.
192///
193/// Candidate names are emitted only when the backend has evidence that the
194/// callable value can reach the call site. `Partial` keeps those proven
195/// candidates while recording that another, opaque source may also reach it.
196#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, 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    /// Declaration-shaped label used only when this function is a tree root.
251    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    /// Source files successfully consumed by the frontend, including files
272    /// that declare no callable functions.
273    pub source_files: BTreeSet<PathBuf>,
274    /// Compiler-provided roots when the backend has authoritative entry
275    /// information. The common graph infers component roots only when empty.
276    pub roots: BTreeSet<SymbolId>,
277}
278
279/// Compatibility name for the per-file graph fragment returned by source
280/// adapters. Project backends return the same language-neutral graph shape.
281pub type FileAnalysis = SemanticCallGraph;
282
283#[derive(Clone, Debug, Eq, PartialEq)]
284pub struct CallNode {
285    pub key: String,
286    /// The edge that introduced this node. Roots have no call-site identity.
287    pub callsite: Option<CallSiteId>,
288    pub label: CallLabel,
289    /// The relationship from the parent node to this node. Roots use `Call`.
290    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    /// A recursive call to an ancestor already visible in the current tree.
300    /// The node key identifies that ancestor; renderers should draw an edge
301    /// back to it rather than expanding its body again.
302    BackEdge,
303}