Skip to main content

graphyn_core/
symbol_id.rs

1//! Symbol identity: the one place that knows how a `SymbolId` is spelled.
2//!
3//! Every adapter mints ids for the symbols it finds and, during resolution,
4//! rewrites placeholder ids into real ones. Both halves of that contract used to
5//! be duplicated per adapter, which let the spellings drift apart — the
6//! TypeScript adapter separated placeholder fields with `|` while the newer
7//! adapters used `::`, a separator that also occurs inside Rust and C++ paths
8//! and therefore could not be parsed back out unambiguously.
9//!
10//! # Resolved ids
11//!
12//! `relative/file/path.rs::SymbolName::kind` — stable, persisted to the store,
13//! and the only form that may appear in a graph node.
14//!
15//! # Placeholder ids
16//!
17//! Emitted by extractors, consumed by resolvers, and never persisted. They use
18//! `|` as a field separator because `|` cannot occur in an identifier or a
19//! module path in any language we parse:
20//!
21//! - `unresolved_import|<module>|<symbol>` — an import awaiting module resolution
22//! - `unresolved_local_type|<type>` — a type reference awaiting local lookup
23//!
24//! [`external_package_id`] is the exception: it survives resolution as a real
25//! graph node, because an edge to a third-party package is a fact worth keeping.
26
27use crate::ir::{Language, Symbol, SymbolId, SymbolKind};
28
29const UNRESOLVED_IMPORT_PREFIX: &str = "unresolved_import";
30const UNRESOLVED_LOCAL_TYPE_PREFIX: &str = "unresolved_local_type";
31const EXTERNAL_PREFIX: &str = "ext";
32
33/// The trailing component of a symbol id, identifying what kind of thing it is.
34pub fn kind_suffix(kind: &SymbolKind) -> &'static str {
35    match kind {
36        SymbolKind::Class => "class",
37        SymbolKind::Interface => "interface",
38        SymbolKind::TypeAlias => "type_alias",
39        SymbolKind::Function => "function",
40        SymbolKind::Method => "method",
41        SymbolKind::Property => "property",
42        SymbolKind::Variable => "variable",
43        SymbolKind::Module => "module",
44        SymbolKind::Enum => "enum",
45        SymbolKind::EnumVariant => "enum_variant",
46        SymbolKind::ExternalPackage => "package",
47    }
48}
49
50/// Mint the canonical id for a symbol defined at `file`.
51pub fn make_symbol_id(file: &str, name: &str, kind: &SymbolKind) -> SymbolId {
52    format!("{file}::{name}::{}", kind_suffix(kind))
53}
54
55/// The id of the synthetic per-file module symbol.
56///
57/// Every file gets one. It anchors file-level edges (imports, includes) that
58/// belong to the file rather than to any symbol inside it.
59pub fn module_symbol_id(file: &str) -> SymbolId {
60    make_symbol_id(file, "module", &SymbolKind::Module)
61}
62
63/// Build the synthetic per-file module symbol.
64pub fn module_symbol(file: &str, language: Language) -> Symbol {
65    Symbol {
66        id: module_symbol_id(file),
67        name: "module".to_string(),
68        kind: SymbolKind::Module,
69        language,
70        file: file.to_string(),
71        line_start: 1,
72        line_end: 1,
73        signature: None,
74    }
75}
76
77/// Split a resolved symbol id back into `(file, name, kind_suffix)`.
78///
79/// File paths may contain `::` on no platform we support, but symbol names can
80/// (`Trait::method` in a Rust signature), so the name is taken as everything
81/// between the first and last separator.
82pub fn parse_symbol_id(id: &str) -> Option<(&str, &str, &str)> {
83    let first = id.find("::")?;
84    let last = id.rfind("::")?;
85    if last <= first {
86        return None;
87    }
88    Some((&id[..first], &id[first + 2..last], &id[last + 2..]))
89}
90
91/// The symbol name component of a resolved id, if it is one.
92pub fn symbol_name_of(id: &str) -> Option<&str> {
93    parse_symbol_id(id).map(|(_, name, _)| name)
94}
95
96// ── placeholders ─────────────────────────────────────────────
97
98/// Placeholder for an import of `symbol` from `module`, pending resolution.
99///
100/// Pass [`IMPORT_ALL`] as `symbol` for a whole-module import (`import os`,
101/// `use foo::*`, `import "fmt"`).
102pub fn unresolved_import_id(module: &str, symbol: &str) -> SymbolId {
103    format!("{UNRESOLVED_IMPORT_PREFIX}|{module}|{symbol}")
104}
105
106/// The `symbol` value denoting "the module itself", not a member of it.
107pub const IMPORT_ALL: &str = "*";
108
109/// Recover `(module, symbol)` from an unresolved-import placeholder.
110pub fn parse_unresolved_import_id(raw: &str) -> Option<(&str, &str)> {
111    let rest = raw.strip_prefix(UNRESOLVED_IMPORT_PREFIX)?.strip_prefix('|')?;
112    // The module may itself be empty (a bare relative import), so split from the
113    // right: the symbol never contains a separator.
114    let cut = rest.rfind('|')?;
115    Some((&rest[..cut], &rest[cut + 1..]))
116}
117
118/// Placeholder for a reference to a type that must be looked up in file scope.
119pub fn unresolved_local_type_id(type_name: &str) -> SymbolId {
120    format!("{UNRESOLVED_LOCAL_TYPE_PREFIX}|{type_name}")
121}
122
123/// Recover the type name from a local-type placeholder.
124pub fn parse_unresolved_local_type_id(raw: &str) -> Option<&str> {
125    raw.strip_prefix(UNRESOLVED_LOCAL_TYPE_PREFIX)?
126        .strip_prefix('|')
127}
128
129/// True if `id` is any placeholder, i.e. resolution did not finish for it.
130///
131/// Placeholders must never reach the graph: [`crate::graph::GraphynGraph::add_relationship`]
132/// drops edges pointing at unknown ids, so an unresolved placeholder is a
133/// silently missing edge. Resolvers use this to decide what to report.
134pub fn is_placeholder(id: &str) -> bool {
135    id.starts_with(UNRESOLVED_IMPORT_PREFIX) || id.starts_with(UNRESOLVED_LOCAL_TYPE_PREFIX)
136}
137
138// ── external packages ────────────────────────────────────────
139
140/// The id of the shared node representing a third-party package.
141///
142/// Unlike placeholders this survives into the graph;
143/// [`crate::graph::GraphynGraph::add_relationship`] creates the node on first
144/// reference.
145pub fn external_package_id(package: &str) -> SymbolId {
146    format!("{EXTERNAL_PREFIX}::{package}::package")
147}
148
149/// True if `id` names an external package node.
150pub fn is_external_package(id: &str) -> bool {
151    id.starts_with("ext::") && id.ends_with("::package")
152}
153
154/// Recover the package name from an external package id.
155pub fn parse_external_package_id(id: &str) -> Option<&str> {
156    id.strip_prefix("ext::")?.strip_suffix("::package")
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn resolved_ids_round_trip() {
165        let id = make_symbol_id("src/models/user.rs", "UserPayload", &SymbolKind::Class);
166        assert_eq!(id, "src/models/user.rs::UserPayload::class");
167        assert_eq!(
168            parse_symbol_id(&id),
169            Some(("src/models/user.rs", "UserPayload", "class"))
170        );
171    }
172
173    #[test]
174    fn symbol_names_containing_the_separator_survive_a_round_trip() {
175        // Rust and C++ both produce names like this for qualified methods.
176        let id = make_symbol_id("src/lib.rs", "Display::fmt", &SymbolKind::Method);
177        assert_eq!(
178            parse_symbol_id(&id),
179            Some(("src/lib.rs", "Display::fmt", "method"))
180        );
181    }
182
183    #[test]
184    fn import_placeholders_round_trip_paths_containing_colons() {
185        // The old `::`-separated placeholder format could not represent this:
186        // splitting on `::` yielded module="crate", symbol="models::UserPayload".
187        let id = unresolved_import_id("crate::models::user_payload", "UserPayload");
188        assert_eq!(
189            parse_unresolved_import_id(&id),
190            Some(("crate::models::user_payload", "UserPayload"))
191        );
192    }
193
194    #[test]
195    fn import_placeholders_round_trip_go_style_module_paths() {
196        let id = unresolved_import_id("github.com/test/app/models", IMPORT_ALL);
197        assert_eq!(
198            parse_unresolved_import_id(&id),
199            Some(("github.com/test/app/models", "*"))
200        );
201    }
202
203    #[test]
204    fn local_type_placeholders_round_trip() {
205        let id = unresolved_local_type_id("ResponseModel");
206        assert_eq!(parse_unresolved_local_type_id(&id), Some("ResponseModel"));
207        assert!(is_placeholder(&id));
208    }
209
210    #[test]
211    fn resolved_ids_are_not_placeholders() {
212        let id = make_symbol_id("src/a.rs", "Alpha", &SymbolKind::Class);
213        assert!(!is_placeholder(&id));
214        assert!(!is_placeholder(&external_package_id("serde")));
215    }
216
217    #[test]
218    fn external_package_ids_round_trip() {
219        let id = external_package_id("serde");
220        assert!(is_external_package(&id));
221        assert_eq!(parse_external_package_id(&id), Some("serde"));
222    }
223}