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}