Skip to main content

sealmap_model/
symbol.rs

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