sealmap_model/symbol.rs
1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5use crate::flow::Flow;
6use crate::path::SourcePath;
7
8/// The canonical, fully qualified identity of a symbol, e.g.
9/// `my_crate::net::Client` or `my_crate::net::Client::connect`.
10///
11/// Ids use `::` as the separator for every language so projections can treat
12/// them uniformly. They are the join key between the model, the rendered
13/// diagrams and the JSON metadata, so an agent can follow an id from a node in
14/// a diagram straight back to its definition.
15///
16/// Symbols that live outside the analysed codebase (standard library, third
17/// party crates, or anything the frontend could not resolve) still get an id;
18/// they are simply absent from [`Codebase::symbols`](crate::Codebase::symbols)
19/// and relations pointing at them carry [`Confidence::External`].
20#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
21#[serde(transparent)]
22pub struct SymbolId(String);
23
24impl SymbolId {
25 /// Wrap a fully qualified path.
26 pub fn new(path: impl Into<String>) -> Self {
27 Self(path.into())
28 }
29
30 /// The id as a string.
31 pub fn as_str(&self) -> &str {
32 &self.0
33 }
34
35 /// The last `::` segment (the short name).
36 ///
37 /// ```
38 /// # use sealmap_model::SymbolId;
39 /// assert_eq!(SymbolId::new("a::b::C").name(), "C");
40 /// ```
41 pub fn name(&self) -> &str {
42 self.0.rsplit("::").next().unwrap_or(&self.0)
43 }
44
45 /// The id with the last segment removed, if there is more than one.
46 ///
47 /// ```
48 /// # use sealmap_model::SymbolId;
49 /// assert_eq!(SymbolId::new("a::b::C").parent().unwrap().as_str(), "a::b");
50 /// assert!(SymbolId::new("a").parent().is_none());
51 /// ```
52 pub fn parent(&self) -> Option<SymbolId> {
53 self.0.rfind("::").map(|i| SymbolId(self.0[..i].to_owned()))
54 }
55
56 /// Append a segment: `a::b` + `c` → `a::b::c`.
57 pub fn child(&self, segment: &str) -> SymbolId {
58 SymbolId(format!("{}::{}", self.0, segment))
59 }
60}
61
62impl fmt::Display for SymbolId {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 f.write_str(&self.0)
65 }
66}
67
68/// What kind of thing a [`Symbol`] is.
69///
70/// The set is a superset of what common languages need; frontends map their
71/// own constructs onto the closest variant (a TypeScript `interface` would be
72/// a [`SymbolKind::Trait`], a Python `class` a [`SymbolKind::Struct`]).
73#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
74#[serde(rename_all = "snake_case")]
75pub enum SymbolKind {
76 /// A module / namespace. Every source file has one.
77 Module,
78 /// A product type with named or positional fields.
79 Struct,
80 /// A sum type.
81 Enum,
82 /// An untagged union.
83 Union,
84 /// An interface / trait / protocol.
85 Trait,
86 /// A type alias.
87 TypeAlias,
88 /// A free function.
89 Function,
90 /// A function attached to a type (inherent or trait method, including
91 /// associated functions without `self`).
92 Method,
93 /// A constant.
94 Const,
95 /// A static / global variable.
96 Static,
97 /// A macro definition.
98 Macro,
99}
100
101impl SymbolKind {
102 /// `true` for kinds that are rendered as classes in a structure diagram.
103 pub fn is_type(self) -> bool {
104 matches!(self, Self::Struct | Self::Enum | Self::Union | Self::Trait | Self::TypeAlias)
105 }
106
107 /// `true` for kinds that have a body and therefore may carry a [`Flow`].
108 pub fn is_callable(self) -> bool {
109 matches!(self, Self::Function | Self::Method)
110 }
111
112 /// Lower-case keyword used in diagrams and metadata (`struct`, `fn`, ...).
113 pub fn keyword(self) -> &'static str {
114 match self {
115 Self::Module => "mod",
116 Self::Struct => "struct",
117 Self::Enum => "enum",
118 Self::Union => "union",
119 Self::Trait => "trait",
120 Self::TypeAlias => "type",
121 Self::Function => "fn",
122 Self::Method => "method",
123 Self::Const => "const",
124 Self::Static => "static",
125 Self::Macro => "macro",
126 }
127 }
128}
129
130/// Visibility of a symbol or member.
131#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133pub enum Visibility {
134 /// Visible to everyone (`pub`).
135 Public,
136 /// Visible within the package / crate (`pub(crate)`).
137 Crate,
138 /// Visible within a named scope (`pub(in path)`, `pub(super)`).
139 Restricted(String),
140 /// Private to the defining scope.
141 Private,
142}
143
144impl Visibility {
145 /// UML-style marker used by Mermaid class diagrams: `+`, `~`, `#` or `-`.
146 pub fn uml_marker(&self) -> char {
147 match self {
148 Self::Public => '+',
149 Self::Crate => '~',
150 Self::Restricted(_) => '#',
151 Self::Private => '-',
152 }
153 }
154}
155
156/// A source range. Lines and columns are **1-based**; `end` is inclusive of
157/// the last line containing the item.
158#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Serialize, Deserialize)]
159pub struct Span {
160 /// First line (1-based).
161 pub start_line: u32,
162 /// Column on the first line (1-based).
163 pub start_col: u32,
164 /// Last line (1-based, inclusive).
165 pub end_line: u32,
166 /// Column on the last line (1-based).
167 pub end_col: u32,
168}
169
170impl Span {
171 /// Construct a span from line/column pairs.
172 pub fn new(start_line: u32, start_col: u32, end_line: u32, end_col: u32) -> Self {
173 Self { start_line, start_col, end_line, end_col }
174 }
175
176 /// `L<start>-L<end>` (or `L<start>` for single-line spans), the compact
177 /// form used in diagram metadata.
178 ///
179 /// ```
180 /// # use sealmap_model::Span;
181 /// assert_eq!(Span::new(3, 1, 9, 2).compact(), "L3-L9");
182 /// assert_eq!(Span::new(4, 1, 4, 9).compact(), "L4");
183 /// ```
184 pub fn compact(&self) -> String {
185 if self.start_line == self.end_line {
186 format!("L{}", self.start_line)
187 } else {
188 format!("L{}-L{}", self.start_line, self.end_line)
189 }
190 }
191}
192
193/// What a [`Member`] is.
194#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
195#[serde(rename_all = "snake_case")]
196pub enum MemberKind {
197 /// A named or positional field.
198 Field,
199 /// An enum variant.
200 Variant,
201 /// An associated constant.
202 AssocConst,
203 /// An associated type.
204 AssocType,
205 /// A required (bodiless) trait method signature.
206 RequiredMethod,
207}
208
209/// A field, variant or associated item listed *inside* a type in structure
210/// diagrams. Methods with bodies are full [`Symbol`]s instead, because they
211/// carry their own [`Flow`] and relations.
212#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
213pub struct Member {
214 /// Member name (`0`, `1`, ... for positional fields).
215 pub name: String,
216 /// What the member is.
217 pub kind: MemberKind,
218 /// Type or signature as written, normalised to single spaces
219 /// (e.g. `Vec<String>`, `(u32, u32)`, `fn(&self) -> u8`).
220 pub ty: Option<String>,
221 /// Member visibility.
222 pub visibility: Visibility,
223 /// Where it is defined.
224 pub span: Span,
225 /// Codebase-relevant types this member's type mentions, resolved to
226 /// canonical ids (primitives and std prelude types are omitted).
227 #[serde(default, skip_serializing_if = "Vec::is_empty")]
228 pub refs: Vec<SymbolId>,
229}
230
231/// A named definition in the codebase.
232#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
233pub struct Symbol {
234 /// Canonical id.
235 pub id: SymbolId,
236 /// Short name.
237 pub name: String,
238 /// Kind of definition.
239 pub kind: SymbolKind,
240 /// Visibility.
241 pub visibility: Visibility,
242 /// File the definition lives in.
243 pub file: SourcePath,
244 /// Where in the file.
245 pub span: Span,
246 /// Enclosing symbol (the type for a method, the module for an item).
247 pub parent: Option<SymbolId>,
248 /// One-line signature, e.g. `pub fn connect(&self, addr: &str) -> Result<Conn>`.
249 pub signature: Option<String>,
250 /// First sentence of the doc comment, if any. Kept short on purpose: it is
251 /// context for an agent, not a copy of the docs.
252 pub doc: Option<String>,
253 /// Generic parameters as written (`T: Clone`, `'a`, `const N: usize`).
254 pub generics: Vec<String>,
255 /// Notable attributes / modifiers: derives (`derive(Debug)`), `async`,
256 /// `unsafe`, `const`, `test`, `cfg(...)`, trait name for trait impls, etc.
257 pub tags: Vec<String>,
258 /// Fields, variants and associated items.
259 pub members: Vec<Member>,
260 /// Ordered control/call flow of the body, for callables.
261 pub flow: Option<Flow>,
262}
263
264impl Symbol {
265 /// A symbol with only the required fields set; everything else empty /
266 /// private. Frontends fill in the rest.
267 pub fn new(id: SymbolId, name: impl Into<String>, kind: SymbolKind, file: SourcePath) -> Self {
268 Self {
269 parent: id.parent(),
270 id,
271 name: name.into(),
272 kind,
273 visibility: Visibility::Private,
274 file,
275 span: Span::default(),
276 signature: None,
277 doc: None,
278 generics: Vec::new(),
279 tags: Vec::new(),
280 members: Vec::new(),
281 flow: None,
282 }
283 }
284}
285
286/// The kind of a directed edge between two symbols.
287#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
288#[serde(rename_all = "snake_case")]
289pub enum RelationKind {
290 /// `from` lexically contains `to` (module → item, type → method).
291 Contains,
292 /// `from` implements trait `to`.
293 Implements,
294 /// Trait `from` has supertrait `to`.
295 Extends,
296 /// `from` has a field whose type mentions `to` (composition / aggregation).
297 FieldType,
298 /// `from`'s signature mentions `to` (parameters, return, bounds).
299 Uses,
300 /// Module `from` imports `to`.
301 Imports,
302 /// Callable `from` calls `to` (aggregated from its [`Flow`]).
303 Calls,
304}
305
306impl RelationKind {
307 /// Lower-case label used in diagrams and metadata.
308 pub fn label(self) -> &'static str {
309 match self {
310 Self::Contains => "contains",
311 Self::Implements => "implements",
312 Self::Extends => "extends",
313 Self::FieldType => "field",
314 Self::Uses => "uses",
315 Self::Imports => "imports",
316 Self::Calls => "calls",
317 }
318 }
319}
320
321/// How sure the frontend is that a relation's target is correct.
322///
323/// Static analysis without type inference cannot resolve everything (method
324/// calls on values of unknown type are the classic case). Rather than drop
325/// such edges or present them as fact, sealmap labels them, so an agent can
326/// weigh them accordingly.
327#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
328#[serde(rename_all = "snake_case")]
329pub enum Confidence {
330 /// Resolved through explicit paths, imports or declared types to a symbol
331 /// defined in the codebase.
332 Exact,
333 /// Matched by name or local inference; probably right, not proven.
334 Inferred,
335 /// Points outside the codebase (std, dependencies) or was not resolved.
336 External,
337}
338
339impl Confidence {
340 /// Single-character marker used in compact metadata (`=`, `~`, `?`).
341 pub fn marker(self) -> char {
342 match self {
343 Self::Exact => '=',
344 Self::Inferred => '~',
345 Self::External => '?',
346 }
347 }
348}
349
350/// A directed, typed edge between two symbols.
351///
352/// Ordering is derived field by field, which is what makes relation sets in a
353/// [`Codebase`](crate::Codebase) iterate deterministically.
354#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
355pub struct Relation {
356 /// Source symbol.
357 pub from: SymbolId,
358 /// Target symbol (may be external).
359 pub to: SymbolId,
360 /// Edge kind.
361 pub kind: RelationKind,
362 /// Resolution confidence.
363 pub confidence: Confidence,
364}
365
366impl Relation {
367 /// Construct a relation.
368 pub fn new(from: SymbolId, to: SymbolId, kind: RelationKind, confidence: Confidence) -> Self {
369 Self { from, to, kind, confidence }
370 }
371}