diffkit-cli 0.1.6

Semantic call-tree diffs for polyglot codebases
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
use std::collections::BTreeSet;
use std::fmt;
use std::path::PathBuf;

use serde::{Deserialize, Serialize};

/// Stable identity of a semantic definition inside one analyzed program.
///
/// `SymbolId` is the public, language-neutral representation of a definition;
/// this alias makes the distinction between definition and edge identities
/// explicit in the graph/diff layers without duplicating the symbol payload.
pub type NodeId = SymbolId;

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
pub struct CallSiteId(pub String);

impl CallSiteId {
    pub fn source(syntax: &CallSyntax, span: &SourceSpan) -> Self {
        Self(format!(
            "{}@{}:{}-{}:{}",
            syntax.key_fragment(),
            span.start_line,
            span.start_column,
            span.end_line,
            span.end_column
        ))
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
pub struct LanguageId(pub String);

impl LanguageId {
    pub fn new(value: impl Into<String>) -> Self {
        Self(value.into())
    }
}

impl fmt::Display for LanguageId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(f)
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Ord, PartialOrd, Hash, Serialize)]
pub struct SymbolId {
    pub language: LanguageId,
    pub module: Vec<String>,
    pub container: Option<String>,
    pub name: String,
}

impl SymbolId {
    pub fn short_name(&self) -> String {
        match &self.container {
            Some(container) => format!("{container}::{}", self.name),
            None => self.name.clone(),
        }
    }

    pub fn qualified_parts(&self) -> Vec<&str> {
        self.module
            .iter()
            .map(String::as_str)
            .chain(self.container.iter().map(String::as_str))
            .chain(std::iter::once(self.name.as_str()))
            .collect()
    }
}

impl fmt::Display for SymbolId {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}://", self.language)?;
        let parts = self.qualified_parts();
        write!(f, "{}", parts.join("::"))
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct SourceSpan {
    pub file: PathBuf,
    pub start_line: usize,
    pub start_column: usize,
    pub start_byte: Option<usize>,
    pub end_line: usize,
    pub end_column: usize,
    pub end_byte: Option<usize>,
}

impl SourceSpan {
    pub fn has_same_coordinates(&self, other: &Self) -> bool {
        self.start_line == other.start_line
            && self.start_column == other.start_column
            && self.end_line == other.end_line
            && self.end_column == other.end_column
    }

    pub fn contains(&self, other: &Self) -> bool {
        (self.start_line, self.start_column) <= (other.start_line, other.start_column)
            && (other.end_line, other.end_column) <= (self.end_line, self.end_column)
    }

    pub fn overlaps(&self, other: &Self) -> bool {
        (self.start_line, self.start_column) <= (other.end_line, other.end_column)
            && (other.start_line, other.start_column) <= (self.end_line, self.end_column)
    }

    /// Stable source-order size used for choosing the tightest enclosing span.
    pub fn extent(&self) -> usize {
        self.end_line.saturating_sub(self.start_line) * 10_000
            + self.end_column.saturating_sub(self.start_column)
    }

    pub fn start_distance(&self, other: &Self) -> usize {
        self.start_line.abs_diff(other.start_line) * 10_000
            + self.start_column.abs_diff(other.start_column)
    }

    pub fn boundary_distance(&self, other: &Self) -> usize {
        self.start_distance(other)
            + self.end_line.abs_diff(other.end_line) * 10_000
            + self.end_column.abs_diff(other.end_column)
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub enum CallSyntax {
    Path(Vec<String>),
    SelfMethod(String),
    Method {
        receiver: String,
        method: String,
    },
    /// An explicit call whose callable is itself an expression (a returned
    /// closure, function-valued field, index, conditional, and so on). The
    /// language backend supplies its semantic target; common syntax fallback
    /// deliberately does not guess one from this display fragment.
    Expression(String),
    /// Syntax written inside an expression-evaluating macro. It is retained
    /// only when the semantic backend confirms a corresponding runtime call;
    /// macro token trees are otherwise not assumed to execute as Rust code.
    CompilerConfirmed(Box<CallSyntax>),
}

impl CallSyntax {
    pub fn key_fragment(&self) -> String {
        match self {
            Self::Path(parts) => parts.join("::"),
            Self::SelfMethod(method) => format!("self.{method}"),
            Self::Method { receiver, method } => format!("{receiver}.{method}"),
            Self::Expression(expression) => expression.clone(),
            Self::CompilerConfirmed(syntax) => {
                format!("compiler-confirmed:{}", syntax.key_fragment())
            }
        }
    }

    pub fn visible(&self) -> &Self {
        match self {
            Self::CompilerConfirmed(syntax) => syntax.visible(),
            syntax => syntax,
        }
    }

    pub fn requires_compiler_confirmation(&self) -> bool {
        matches!(self, Self::CompilerConfirmed(_))
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CallSite {
    /// Identity of this source call edge. Backends derive it from the owning
    /// definition and the call's structural source identity. It is kept
    /// separate from the callee because one function may call the same target
    /// more than once.
    pub id: CallSiteId,
    pub syntax: CallSyntax,
    /// Authoritative semantic target supplied by rustc_public, OCaml Typedtree, or
    /// another language backend. Syntax-only frontends use `Unresolved` and
    /// the common graph falls back to conservative path resolution.
    pub target: CallTarget,
    /// Fully formatted by the owning language frontend. Core rendering never
    /// needs to know whether a call uses `f(x)` or `f x` syntax.
    pub label: CallLabel,
    pub span: SourceSpan,
}

#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum CallTarget {
    #[default]
    Unresolved,
    /// A typed indirect call whose concrete callable value is not known.
    /// This is intentionally distinct from a syntax-resolution failure.
    Indirect {
        signature: Option<String>,
        reason: UnresolvedReason,
    },
    Direct(SymbolId),
    Dynamic {
        dispatch: SymbolId,
        candidates: Vec<DispatchCandidate>,
        resolution: DispatchResolution,
        evidence: DispatchEvidence,
        unresolved_reasons: BTreeSet<UnresolvedReason>,
    },
}

impl CallTarget {
    pub fn dynamic(
        dispatch: SymbolId,
        candidates: Vec<DispatchCandidate>,
        evidence: DispatchEvidence,
        unresolved_reasons: BTreeSet<UnresolvedReason>,
    ) -> Self {
        let resolution = DispatchResolution::from_candidates(
            !candidates.is_empty(),
            !unresolved_reasons.is_empty(),
        );
        Self::Dynamic {
            dispatch,
            candidates,
            resolution,
            evidence,
            unresolved_reasons,
        }
    }
}

/// Why a candidate set is justified when it is complete.
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub enum DispatchEvidence {
    /// Concrete values were followed to this exact call site.
    #[default]
    ExactFlow,
    /// The backend proved a closed candidate universe for this call site.
    ClosedSet,
}

impl fmt::Display for DispatchEvidence {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Self::ExactFlow => "exact-flow",
            Self::ClosedSet => "closed-set",
        })
    }
}

/// Machine-readable causes for an unresolved or partially resolved call.
#[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub enum UnresolvedReason {
    OpaqueInput,
    ExternalCode,
    ExternalMemory,
    FunctionPointer,
    CrossCrateBoundary,
    AnalysisLimit,
    UnsupportedConstruct,
}

impl fmt::Display for UnresolvedReason {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let text = match self {
            Self::OpaqueInput => "opaque input",
            Self::ExternalCode => "external code",
            Self::ExternalMemory => "external memory",
            Self::FunctionPointer => "function pointer",
            Self::CrossCrateBoundary => "cross-crate boundary",
            Self::AnalysisLimit => "analysis limit",
            Self::UnsupportedConstruct => "unsupported construct",
        };
        f.write_str(text)
    }
}

/// How completely a language backend resolved an indirect call site.
///
/// Candidate names are emitted only when the backend has evidence that the
/// callable value can reach the call site. `Partial` keeps those proven
/// candidates while recording that another, opaque source may also reach it.
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)]
pub enum DispatchResolution {
    #[default]
    Complete,
    Partial,
    Unresolved,
}

impl DispatchResolution {
    pub fn from_candidates(has_candidates: bool, has_unresolved_sources: bool) -> Self {
        match (has_candidates, has_unresolved_sources) {
            (false, _) => Self::Unresolved,
            (true, true) => Self::Partial,
            (true, false) => Self::Complete,
        }
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct DispatchCandidate {
    pub target: SymbolId,
    pub label: CallLabel,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct CallLabel {
    pub default: String,
    pub typed: Option<String>,
}

impl CallLabel {
    pub fn new(default: impl Into<String>) -> Self {
        Self {
            default: default.into(),
            typed: None,
        }
    }

    pub fn with_types(default: impl Into<String>, typed: impl Into<String>) -> Self {
        Self {
            default: default.into(),
            typed: Some(typed.into()),
        }
    }

    pub fn text(&self, show_types: bool) -> &str {
        if show_types {
            self.typed.as_deref().unwrap_or(&self.default)
        } else {
            &self.default
        }
    }

    pub fn with_suffix(&self, suffix: &str) -> Self {
        Self {
            default: format!("{}{suffix}", self.default),
            typed: self.typed.as_ref().map(|typed| format!("{typed}{suffix}")),
        }
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct FunctionInfo {
    pub id: SymbolId,
    /// Declaration-shaped label used only when this function is a tree root.
    pub label: CallLabel,
    pub public: bool,
    pub calls: Vec<CallSite>,
    pub span: SourceSpan,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct LanguageFact {
    pub subject: SymbolId,
    pub namespace: LanguageId,
    pub kind: String,
    pub key: String,
    pub value: String,
    pub span: SourceSpan,
}

#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct SemanticCallGraph {
    pub functions: Vec<FunctionInfo>,
    pub facts: Vec<LanguageFact>,
    /// Source files successfully consumed by the frontend, including files
    /// that declare no callable functions.
    pub source_files: BTreeSet<PathBuf>,
    /// Compiler-provided roots when the backend has authoritative entry
    /// information. The common graph infers component roots only when empty.
    pub roots: BTreeSet<SymbolId>,
}

impl SemanticCallGraph {
    /// Merge a frontend fragment without exposing storage details to each
    /// language implementation. This is the common composition point for
    /// multi-file projects.
    pub fn append(&mut self, mut fragment: Self) {
        self.functions.append(&mut fragment.functions);
        self.facts.append(&mut fragment.facts);
        self.source_files.append(&mut fragment.source_files);
        self.roots.append(&mut fragment.roots);
    }
}

/// Compatibility name for the per-file graph fragment returned by source
/// adapters. Project backends return the same language-neutral graph shape.
pub type FileAnalysis = SemanticCallGraph;

#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CallNode {
    pub key: String,
    /// The edge that introduced this node. Roots have no call-site identity.
    pub callsite: Option<CallSiteId>,
    pub label: CallLabel,
    /// The relationship from the parent node to this node. Roots use `Call`.
    pub relation: CallRelation,
    pub children: Vec<CallNode>,
}

#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
pub enum CallRelation {
    #[default]
    Call,
    DispatchCandidate,
    /// A recursive call to an ancestor already visible in the current tree.
    /// The node key identifies that ancestor; renderers should draw an edge
    /// back to it rather than expanding its body again.
    BackEdge,
}