Skip to main content

dbg_cli/session_db/canonicalizer/
go.rs

1//! Go symbol canonicalization.
2//!
3//! Go emits frames in forms like:
4//!   * `main.handleRequest`
5//!   * `main.(*Server).handleRequest`              — pointer receiver
6//!   * `main.Server.handleRequest`                 — value receiver
7//!   * `main.handleRequest.func1`                  — closure inside func
8//!   * `github.com/foo/bar.(*Client).Do`           — full import path
9//!   * `runtime.goexit`                            — runtime built-ins
10//!   * `type:.eq.[...]`                            — auto-generated
11//!
12//! Canonical form preserves pointer-vs-value receiver distinction
13//! (they're different methods in Go). Closures (`.func1`, `.func2`) are
14//! flagged synthetic — their numeric suffix isn't stable across edits.
15
16use super::{CanonicalSymbol, Canonicalizer};
17
18pub struct GoCanonicalizer;
19
20impl Canonicalizer for GoCanonicalizer {
21    fn lang(&self) -> &'static str {
22        "go"
23    }
24
25    fn canonicalize(&self, raw: &str) -> CanonicalSymbol {
26        let synthetic = is_synthetic(raw);
27        CanonicalSymbol {
28            lang: "go",
29            fqn: raw.to_string(),
30            file: None,
31            line: None,
32            demangled: None,
33            raw: raw.to_string(),
34            is_synthetic: synthetic,
35        }
36    }
37}
38
39fn is_synthetic(s: &str) -> bool {
40    // `.func1`, `.func2`, ... — compiler-generated closure names.
41    // They're the only common synthetic form; everything else is the
42    // user's symbol verbatim.
43    if let Some(tail) = s.rsplit('.').next() {
44        if let Some(rest) = tail.strip_prefix("func") {
45            if !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()) {
46                return true;
47            }
48        }
49    }
50    // Auto-generated type equality / hash helpers.
51    s.starts_with("type:.eq.") || s.starts_with("type:.hash.")
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    fn g() -> GoCanonicalizer { GoCanonicalizer }
59
60    #[test]
61    fn plain_function_preserved() {
62        let s = g().canonicalize("main.handleRequest");
63        assert_eq!(s.fqn, "main.handleRequest");
64        assert_eq!(s.lang, "go");
65        assert!(!s.is_synthetic);
66    }
67
68    #[test]
69    fn pointer_and_value_receivers_distinct() {
70        let ptr = g().canonicalize("main.(*Server).handleRequest");
71        let val = g().canonicalize("main.Server.handleRequest");
72        assert_ne!(ptr.fqn, val.fqn);
73    }
74
75    #[test]
76    fn full_import_path_preserved() {
77        let s = g().canonicalize("github.com/foo/bar.(*Client).Do");
78        assert_eq!(s.fqn, "github.com/foo/bar.(*Client).Do");
79    }
80
81    #[test]
82    fn numeric_closure_is_synthetic() {
83        let s = g().canonicalize("main.handleRequest.func1");
84        assert!(s.is_synthetic, "{s:?}");
85    }
86
87    #[test]
88    fn multidigit_closure_is_synthetic() {
89        let s = g().canonicalize("main.handleRequest.func42");
90        assert!(s.is_synthetic);
91    }
92
93    #[test]
94    fn func_in_name_is_not_synthetic() {
95        // `.func` alone (no digit) should NOT be considered synthetic —
96        // a user could legitimately name a method `func`.
97        let s = g().canonicalize("pkg.Service.func");
98        assert!(!s.is_synthetic);
99    }
100
101    #[test]
102    fn runtime_symbol_not_synthetic() {
103        let s = g().canonicalize("runtime.goexit");
104        assert!(!s.is_synthetic);
105    }
106
107    #[test]
108    fn type_eq_synthetic() {
109        let s = g().canonicalize("type:.eq.[1024]uint8");
110        assert!(s.is_synthetic);
111    }
112
113    #[test]
114    fn key_is_lang_plus_fqn() {
115        let s = g().canonicalize("main.f");
116        assert_eq!(s.key(), ("go", "main.f"));
117    }
118}