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
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    /// Stable source-order size used for choosing the tightest enclosing span.
109    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    /// An explicit call whose callable is itself an expression (a returned
135    /// closure, function-valued field, index, conditional, and so on). The
136    /// language backend supplies its semantic target; common syntax fallback
137    /// deliberately does not guess one from this display fragment.
138    Expression(String),
139    /// Syntax written inside an expression-evaluating macro. It is retained
140    /// only when the semantic backend confirms a corresponding runtime call;
141    /// macro token trees are otherwise not assumed to execute as Rust code.
142    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    /// Identity of this source call edge. Backends derive it from the owning
173    /// definition and the call's structural source identity. It is kept
174    /// separate from the callee because one function may call the same target
175    /// more than once.
176    pub id: CallSiteId,
177    pub syntax: CallSyntax,
178    /// Authoritative semantic target supplied by rustc_public, OCaml Typedtree, or
179    /// another language backend. Syntax-only frontends use `Unresolved` and
180    /// the common graph falls back to conservative path resolution.
181    pub target: CallTarget,
182    /// Fully formatted by the owning language frontend. Core rendering never
183    /// needs to know whether a call uses `f(x)` or `f x` syntax.
184    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    /// A typed indirect call whose concrete callable value is not known.
193    /// This is intentionally distinct from a syntax-resolution failure.
194    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/// Why a candidate set is justified when it is complete.
230#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
231pub enum DispatchEvidence {
232    /// Concrete values were followed to this exact call site.
233    #[default]
234    ExactFlow,
235    /// The backend proved a closed candidate universe for this call site.
236    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/// Machine-readable causes for an unresolved or partially resolved call.
249#[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/// How completely a language backend resolved an indirect call site.
276///
277/// Candidate names are emitted only when the backend has evidence that the
278/// callable value can reach the call site. `Partial` keeps those proven
279/// candidates while recording that another, opaque source may also reach it.
280#[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    /// Declaration-shaped label used only when this function is a tree root.
345    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    /// Source files successfully consumed by the frontend, including files
366    /// that declare no callable functions.
367    pub source_files: BTreeSet<PathBuf>,
368    /// Compiler-provided roots when the backend has authoritative entry
369    /// information. The common graph infers component roots only when empty.
370    pub roots: BTreeSet<SymbolId>,
371}
372
373impl SemanticCallGraph {
374    /// Merge a frontend fragment without exposing storage details to each
375    /// language implementation. This is the common composition point for
376    /// multi-file projects.
377    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
385/// Compatibility name for the per-file graph fragment returned by source
386/// adapters. Project backends return the same language-neutral graph shape.
387pub type FileAnalysis = SemanticCallGraph;
388
389#[derive(Clone, Debug, Eq, PartialEq)]
390pub struct CallNode {
391    pub key: String,
392    /// The edge that introduced this node. Roots have no call-site identity.
393    pub callsite: Option<CallSiteId>,
394    pub label: CallLabel,
395    /// The relationship from the parent node to this node. Roots use `Call`.
396    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    /// A recursive call to an ancestor already visible in the current tree.
406    /// The node key identifies that ancestor; renderers should draw an edge
407    /// back to it rather than expanding its body again.
408    BackEdge,
409}