Skip to main content

repo_mapper/
tag.rs

1//! Tag and TagKind types (SPEC §2.1).
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7/// The kind of tag: definition or reference (SPEC §2.1).
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
9pub enum TagKind {
10    /// A definition (function, class, etc.)
11    Def,
12    /// A reference to an identifier
13    Ref,
14}
15
16impl fmt::Display for TagKind {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self {
19            TagKind::Def => write!(f, "def"),
20            TagKind::Ref => write!(f, "ref"),
21        }
22    }
23}
24
25impl std::str::FromStr for TagKind {
26    type Err = ();
27
28    fn from_str(s: &str) -> Result<Self, Self::Err> {
29        match s {
30            "def" => Ok(TagKind::Def),
31            "ref" => Ok(TagKind::Ref),
32            _ => Err(()),
33        }
34    }
35}
36
37/// A tag extracted from source code (SPEC §2.1).
38///
39/// Field order is significant for derived `PartialOrd`/`Ord`:
40/// tags sort by (rel_fname, fname, line, name, kind).
41#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
42pub struct Tag {
43    /// Path relative to repository root (SPEC §2.2)
44    pub rel_fname: String,
45    /// Absolute path to file
46    pub fname: String,
47    /// 1-indexed line number, or -1 for fallback refs (SPEC §3.4)
48    pub line: i32,
49    /// The identifier text
50    pub name: String,
51    /// Definition or reference
52    pub kind: TagKind,
53}
54
55impl Tag {
56    /// Create a new tag.
57    pub fn new(
58        rel_fname: impl Into<String>,
59        fname: impl Into<String>,
60        line: i32,
61        name: impl Into<String>,
62        kind: TagKind,
63    ) -> Self {
64        Self {
65            rel_fname: rel_fname.into(),
66            fname: fname.into(),
67            line,
68            name: name.into(),
69            kind,
70        }
71    }
72
73    /// Create a definition tag.
74    pub fn def(
75        rel_fname: impl Into<String>,
76        fname: impl Into<String>,
77        line: i32,
78        name: impl Into<String>,
79    ) -> Self {
80        Self::new(rel_fname, fname, line, name, TagKind::Def)
81    }
82
83    /// Create a reference tag.
84    pub fn reference(
85        rel_fname: impl Into<String>,
86        fname: impl Into<String>,
87        line: i32,
88        name: impl Into<String>,
89    ) -> Self {
90        Self::new(rel_fname, fname, line, name, TagKind::Ref)
91    }
92
93    /// Check if this is a definition tag.
94    pub fn is_def(&self) -> bool {
95        self.kind == TagKind::Def
96    }
97
98    /// Check if this is a reference tag.
99    pub fn is_ref(&self) -> bool {
100        self.kind == TagKind::Ref
101    }
102}
103
104impl fmt::Display for Tag {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        write!(
107            f,
108            "{}:{}:{}:{}",
109            self.rel_fname, self.line, self.name, self.kind
110        )
111    }
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    #[test]
119    fn tag_kind_display() {
120        assert_eq!(TagKind::Def.to_string(), "def");
121        assert_eq!(TagKind::Ref.to_string(), "ref");
122    }
123
124    #[test]
125    fn tag_kind_from_str() {
126        assert_eq!("def".parse::<TagKind>(), Ok(TagKind::Def));
127        assert_eq!("ref".parse::<TagKind>(), Ok(TagKind::Ref));
128        assert!("other".parse::<TagKind>().is_err());
129    }
130
131    #[test]
132    fn tag_construction() {
133        let tag = Tag::def("src/main.rs", "/home/user/proj/src/main.rs", 10, "main");
134        assert_eq!(tag.rel_fname, "src/main.rs");
135        assert_eq!(tag.fname, "/home/user/proj/src/main.rs");
136        assert_eq!(tag.line, 10);
137        assert_eq!(tag.name, "main");
138        assert!(tag.is_def());
139        assert!(!tag.is_ref());
140    }
141
142    #[test]
143    fn tag_ordering() {
144        // Tags sort by (rel_fname, fname, line, name, kind)
145        let t1 = Tag::def("a.rs", "/a.rs", 1, "foo");
146        let t2 = Tag::def("a.rs", "/a.rs", 2, "foo");
147        let t3 = Tag::def("b.rs", "/b.rs", 1, "foo");
148
149        assert!(t1 < t2); // same file, different line
150        assert!(t2 < t3); // different file
151    }
152
153    #[test]
154    fn tag_fallback_line() {
155        // SPEC §3.2: fallback refs use line = -1
156        let tag = Tag::reference("a.rs", "/a.rs", -1, "ident");
157        assert_eq!(tag.line, -1);
158    }
159}