Skip to main content

projectatlas_core/
symbols.rs

1//! Purpose: Define `ProjectAtlas` symbol graph domain types.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Source label for a parser relation outside a named declaration or namespace.
7pub const MODULE_RELATION_SOURCE: &str = "<module>";
8
9/// Kind of symbol stored in the `ProjectAtlas` graph.
10#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
11#[serde(rename_all = "kebab-case")]
12pub enum SymbolKind {
13    /// A free function or language-level function declaration.
14    Function,
15    /// A method declaration associated with a type or class.
16    Method,
17    /// A class declaration.
18    Class,
19    /// A Rust-style struct or record declaration.
20    Struct,
21    /// An enum declaration.
22    Enum,
23    /// A trait declaration.
24    Trait,
25    /// An interface declaration.
26    Interface,
27    /// A module, namespace, package, or source unit.
28    Module,
29    /// A documentation heading with an exact source selector.
30    Heading,
31    /// A type alias or type declaration.
32    Type,
33    /// A constant, static, field, or variable declaration worth indexing.
34    Value,
35    /// An import, use, include, using, or package dependency edge source.
36    Import,
37    /// A package manifest entry such as a Cargo package.
38    Package,
39    /// A workspace manifest entry such as a Cargo workspace.
40    Workspace,
41    /// A dependency declared in a manifest.
42    Dependency,
43    /// A symbol that did not map cleanly to a richer kind.
44    Unknown,
45}
46
47impl fmt::Display for SymbolKind {
48    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
49        formatter.write_str(match self {
50            Self::Function => "function",
51            Self::Method => "method",
52            Self::Class => "class",
53            Self::Struct => "struct",
54            Self::Enum => "enum",
55            Self::Trait => "trait",
56            Self::Interface => "interface",
57            Self::Module => "module",
58            Self::Heading => "heading",
59            Self::Type => "type",
60            Self::Value => "value",
61            Self::Import => "import",
62            Self::Package => "package",
63            Self::Workspace => "workspace",
64            Self::Dependency => "dependency",
65            Self::Unknown => "unknown",
66        })
67    }
68}
69
70impl SymbolKind {
71    /// Parse a persisted symbol kind.
72    #[must_use]
73    pub fn from_db(value: &str) -> Self {
74        match value {
75            "function" => Self::Function,
76            "method" => Self::Method,
77            "class" => Self::Class,
78            "struct" => Self::Struct,
79            "enum" => Self::Enum,
80            "trait" => Self::Trait,
81            "interface" => Self::Interface,
82            "module" => Self::Module,
83            "heading" => Self::Heading,
84            "type" => Self::Type,
85            "value" => Self::Value,
86            "import" => Self::Import,
87            "package" => Self::Package,
88            "workspace" => Self::Workspace,
89            "dependency" => Self::Dependency,
90            _ => Self::Unknown,
91        }
92    }
93}
94
95/// Kind of graph relation stored for symbols.
96#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
97#[serde(rename_all = "kebab-case")]
98pub enum RelationKind {
99    /// One symbol contains another symbol.
100    Contains,
101    /// A source imports or includes another module.
102    Imports,
103    /// A source symbol calls a target symbol or expression.
104    Calls,
105    /// A package or manifest depends on another package.
106    DependsOn,
107}
108
109impl fmt::Display for RelationKind {
110    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
111        formatter.write_str(match self {
112            Self::Contains => "contains",
113            Self::Imports => "imports",
114            Self::Calls => "calls",
115            Self::DependsOn => "depends-on",
116        })
117    }
118}
119
120impl RelationKind {
121    /// Parse a persisted relation kind.
122    #[must_use]
123    pub fn from_db(value: &str) -> Option<Self> {
124        match value {
125            "contains" => Some(Self::Contains),
126            "imports" => Some(Self::Imports),
127            "calls" => Some(Self::Calls),
128            "depends-on" => Some(Self::DependsOn),
129            _ => None,
130        }
131    }
132}
133
134/// Parser strategy used to produce a graph entry.
135#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
136#[serde(rename_all = "kebab-case")]
137pub enum ParserKind {
138    /// A tree-sitter grammar produced the result.
139    TreeSitter,
140    /// A manifest parser produced the result.
141    Manifest,
142    /// A deterministic structural adapter produced the result.
143    Structural,
144    /// A conservative regex fallback produced the result.
145    Fallback,
146}
147
148impl fmt::Display for ParserKind {
149    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
150        formatter.write_str(match self {
151            Self::TreeSitter => "tree-sitter",
152            Self::Manifest => "manifest",
153            Self::Structural => "structural",
154            Self::Fallback => "fallback",
155        })
156    }
157}
158
159impl ParserKind {
160    /// Parse a persisted parser kind.
161    #[must_use]
162    pub fn from_db(value: &str) -> Self {
163        match value {
164            "tree-sitter" => Self::TreeSitter,
165            "manifest" => Self::Manifest,
166            "structural" => Self::Structural,
167            _ => Self::Fallback,
168        }
169    }
170}
171
172/// Exact parser-supplied byte and column selector for one symbol.
173#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
174pub struct SymbolSourceSelector {
175    /// Inclusive UTF-8 byte offset.
176    pub byte_start: usize,
177    /// Exclusive UTF-8 byte offset.
178    pub byte_end: usize,
179    /// Zero-based Unicode-scalar start column.
180    pub column_start: usize,
181    /// Exclusive zero-based Unicode-scalar end column.
182    pub column_end: usize,
183}
184
185/// A code or manifest symbol indexed by `ProjectAtlas`.
186#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
187pub struct CodeSymbol {
188    /// Repository-relative file path.
189    pub path: String,
190    /// Detected language or file family.
191    pub language: Option<String>,
192    /// Symbol name.
193    pub name: String,
194    /// Symbol kind.
195    pub kind: SymbolKind,
196    /// Compact declaration signature or source row.
197    pub signature: String,
198    /// Whether the declaration is exported or publicly visible.
199    pub exported: bool,
200    /// Extracted doc comment or docstring associated with the symbol.
201    pub documentation: Option<String>,
202    /// One-based start line.
203    pub line_start: usize,
204    /// One-based end line.
205    pub line_end: usize,
206    /// Exact byte and column selector when the parser supplies one.
207    #[serde(default, skip_serializing_if = "Option::is_none")]
208    pub source_selector: Option<SymbolSourceSelector>,
209    /// Optional containing symbol name.
210    pub parent: Option<String>,
211    /// Parser strategy that produced this symbol.
212    pub parser: ParserKind,
213    /// Optional detail, usually the original parser node kind.
214    pub detail: Option<String>,
215}
216
217/// A directed relation between symbols or source-level references.
218#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
219pub struct SymbolRelation {
220    /// Repository-relative file path.
221    pub path: String,
222    /// Source symbol name or module sentinel.
223    pub source_name: String,
224    /// Target symbol, import path, or dependency name.
225    pub target_name: String,
226    /// Relation kind.
227    pub kind: RelationKind,
228    /// One-based line where the relation appears.
229    pub line: usize,
230    /// Compact source context for the relation.
231    pub context: String,
232    /// Parser strategy that produced this relation.
233    pub parser: ParserKind,
234}
235
236/// Symbol graph extracted from one file.
237#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
238pub struct SymbolGraph {
239    /// Repository-relative file path.
240    pub path: String,
241    /// Detected language or file family.
242    pub language: Option<String>,
243    /// Primary parser strategy used for the file.
244    pub parser: ParserKind,
245    /// Extracted declaration and manifest symbols.
246    pub symbols: Vec<CodeSymbol>,
247    /// Extracted import, dependency, containment, and call relations.
248    pub relations: Vec<SymbolRelation>,
249}
250
251/// File-level parser metadata persisted even when a graph has no symbols.
252#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
253pub struct SourceParseMetadata {
254    /// Repository-relative file path.
255    pub path: String,
256    /// Detected language or file family.
257    pub language: Option<String>,
258    /// Primary parser strategy used for the file.
259    pub parser: ParserKind,
260    /// Number of declaration or manifest symbols emitted for this file.
261    pub symbol_count: usize,
262    /// Number of relations emitted for this file.
263    pub relation_count: usize,
264}
265
266impl SourceParseMetadata {
267    /// Build persisted parser metadata from a graph.
268    #[must_use]
269    pub fn from_graph(graph: &SymbolGraph) -> Self {
270        Self {
271            path: graph.path.clone(),
272            language: graph.language.clone(),
273            parser: graph.parser,
274            symbol_count: graph.symbols.len(),
275            relation_count: graph.relations.len(),
276        }
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn heading_symbol_kind_round_trips_without_changing_unknown_fallback() {
286        assert_eq!(SymbolKind::Heading.to_string(), "heading");
287        assert_eq!(SymbolKind::from_db("heading"), SymbolKind::Heading);
288        assert!(matches!(
289            serde_json::to_string(&SymbolKind::Heading).as_deref(),
290            Ok("\"heading\"")
291        ));
292        assert!(matches!(
293            serde_json::from_str::<SymbolKind>("\"heading\""),
294            Ok(SymbolKind::Heading)
295        ));
296        assert_eq!(SymbolKind::from_db("future-kind"), SymbolKind::Unknown);
297    }
298}