Skip to main content

gobject_ast/model/
type_info.rs

1use serde::Serialize;
2
3fn is_zero(v: &usize) -> bool {
4    *v == 0
5}
6
7use crate::model::{SourceLocation, types::BasicType};
8
9#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
10#[serde(rename_all = "snake_case")]
11pub enum AutoCleanupMacro {
12    /// g_autoptr(TypeName)
13    Autoptr(String),
14    /// g_auto(TypeName)
15    Auto(String),
16    /// g_autofree
17    Autofree,
18    /// g_autolist(TypeName)
19    Autolist(String),
20    /// g_autoslist(TypeName)
21    Autoslist(String),
22    /// g_autoqueue(TypeName)
23    Autoqueue(String),
24}
25
26impl AutoCleanupMacro {
27    /// Get the macro name as it would appear in documentation
28    pub fn name(&self) -> &'static str {
29        match self {
30            Self::Autoptr(_) => "g_autoptr",
31            Self::Auto(_) => "g_auto",
32            Self::Autofree => "g_autofree",
33            Self::Autolist(_) => "g_autolist",
34            Self::Autoslist(_) => "g_autoslist",
35            Self::Autoqueue(_) => "g_autoqueue",
36        }
37    }
38
39    /// Get the type argument for macros that take one (None for g_autofree)
40    pub fn type_arg(&self) -> Option<&str> {
41        match self {
42            Self::Autoptr(t)
43            | Self::Auto(t)
44            | Self::Autolist(t)
45            | Self::Autoslist(t)
46            | Self::Autoqueue(t) => Some(t),
47            Self::Autofree => None,
48        }
49    }
50}
51
52impl std::fmt::Display for AutoCleanupMacro {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        match self {
55            Self::Autoptr(t) => f.write_fmt(format_args!("g_autoptr({t})")),
56            Self::Auto(t) => f.write_fmt(format_args!("g_auto({t})")),
57            Self::Autofree => f.write_str("g_autofree"),
58            Self::Autolist(t) => f.write_fmt(format_args!("g_autolist({t})")),
59            Self::Autoslist(t) => f.write_fmt(format_args!("g_autoslist({t})")),
60            Self::Autoqueue(t) => f.write_fmt(format_args!("g_autoqueue({t})")),
61        }
62    }
63}
64
65#[derive(Debug, Clone, Serialize)]
66pub struct TypeInfo {
67    /// Base type without qualifiers or pointers: `"GFile"`, `"int"`.
68    pub base_type: String,
69    #[serde(skip_serializing_if = "std::ops::Not::not")]
70    pub is_const: bool,
71    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
72    pub is_volatile: bool,
73    /// True when spelled with the `struct` keyword (`struct Foo *`).
74    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
75    pub is_struct: bool,
76    /// True when spelled with the `union` keyword (`union Foo *`).
77    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
78    pub is_union: bool,
79    /// Pointer indirections: 0 = value, 1 = `*`, 2 = `**`.
80    #[serde(skip_serializing_if = "is_zero")]
81    pub pointer_depth: usize,
82    /// Full type string as it appears in source (`"const GFile *"`).
83    #[serde(skip)]
84    pub full_text: String,
85    pub location: SourceLocation,
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub auto_cleanup: Option<AutoCleanupMacro>,
88}
89
90impl TypeInfo {
91    pub fn new(type_string: &str, location: SourceLocation) -> Self {
92        let trimmed = type_string.trim();
93        let auto_cleanup = Self::parse_auto_cleanup(trimmed);
94
95        let parts: Vec<&str> = trimmed.split_whitespace().collect();
96        let mut filtered_parts: Vec<&str> = Vec::new();
97        let mut is_const = false;
98
99        let macro_name = auto_cleanup.as_ref().map(AutoCleanupMacro::name);
100        let mut is_volatile = false;
101
102        for part in &parts {
103            match *part {
104                "static" | "extern" | "inline" => {}
105                "const" => {
106                    is_const = true;
107                    filtered_parts.push(part);
108                }
109                "volatile" => {
110                    is_volatile = true;
111                    filtered_parts.push(part);
112                }
113                // Drop the macro token(s): bare name ("g_autoptr"), compact
114                // form ("g_autoptr(Foo)"), or the arg token ("(Foo)") from
115                // the spaced form "g_autoptr (Foo)".
116                _ if macro_name
117                    .is_some_and(|name| part.starts_with(name) || part.starts_with('(')) => {}
118                _ => {
119                    filtered_parts.push(part);
120                }
121            }
122        }
123
124        let cleaned = filtered_parts.join(" ");
125
126        // Strip const/volatile in any order to reach the base type.
127        let without_qualifiers: String = cleaned
128            .split_whitespace()
129            .filter(|&w| w != "const" && w != "volatile")
130            .collect::<Vec<_>>()
131            .join(" ");
132
133        let pointer_depth = without_qualifiers.chars().filter(|&c| c == '*').count();
134
135        let raw_base = without_qualifiers.replace('*', "").trim().to_string();
136        let (base_type, is_struct, is_union) = if let Some(ref auto) = auto_cleanup {
137            if let Some(type_arg) = auto.type_arg() {
138                (type_arg.to_owned(), false, false)
139            } else {
140                Self::extract_base_type(&raw_base)
141            }
142        } else {
143            Self::extract_base_type(&raw_base)
144        };
145
146        Self {
147            base_type,
148            is_const,
149            is_volatile,
150            is_struct,
151            is_union,
152            pointer_depth,
153            full_text: cleaned,
154            location,
155            auto_cleanup,
156        }
157    }
158
159    fn extract_base_type(raw_base: &str) -> (String, bool, bool) {
160        if let Some(rest) = raw_base.strip_prefix("struct ") {
161            (rest.trim().to_string(), true, false)
162        } else if let Some(rest) = raw_base.strip_prefix("union ") {
163            (rest.trim().to_string(), false, true)
164        } else {
165            (raw_base.to_string(), false, false)
166        }
167    }
168
169    fn parse_auto_cleanup(type_str: &str) -> Option<AutoCleanupMacro> {
170        let try_with_arg = |macro_name: &str| -> Option<String> {
171            let pos = type_str.find(macro_name)?;
172            let after_name = type_str[pos + macro_name.len()..].trim_start();
173            if !after_name.starts_with('(') {
174                return None;
175            }
176            let inner = &after_name[1..];
177            let end = inner.find(')')?;
178            Some(inner[..end].trim().to_string())
179        };
180
181        if type_str.contains("g_autofree") {
182            Some(AutoCleanupMacro::Autofree)
183        } else if let Some(t) = try_with_arg("g_autoptr") {
184            Some(AutoCleanupMacro::Autoptr(t))
185        } else if let Some(t) = try_with_arg("g_autolist") {
186            Some(AutoCleanupMacro::Autolist(t))
187        } else if let Some(t) = try_with_arg("g_autoslist") {
188            Some(AutoCleanupMacro::Autoslist(t))
189        } else if let Some(t) = try_with_arg("g_autoqueue") {
190            Some(AutoCleanupMacro::Autoqueue(t))
191        } else {
192            try_with_arg("g_auto").map(AutoCleanupMacro::Auto)
193        }
194    }
195
196    /// Check if this is a pointer type (at least one level of indirection)
197    pub fn is_pointer(&self) -> bool {
198        self.pointer_depth > 0
199    }
200
201    /// Get the base type without any qualifiers or pointers
202    pub fn base_type_name(&self) -> &str {
203        &self.base_type
204    }
205
206    /// Check if the base type matches the given name
207    pub fn is_base_type(&self, name: &str) -> bool {
208        self.base_type == name
209    }
210
211    /// Check if the type contains the given string (in full text)
212    pub fn contains(&self, pattern: &str) -> bool {
213        self.full_text.contains(pattern)
214    }
215
216    /// Check if the type uses any auto-cleanup macro (g_autoptr, g_autofree,
217    /// g_autolist, etc.)
218    pub fn uses_auto_cleanup(&self) -> bool {
219        self.auto_cleanup.is_some()
220    }
221
222    /// GLib C aliases normalised to their C equivalents (`gint` → `int`).
223    pub fn normalized_base_type(&self) -> &str {
224        match self.base_type.as_str() {
225            "gint" => "int",
226            "guint" => "unsigned int",
227            "glong" => "long",
228            "gulong" => "unsigned long",
229            "gshort" => "short",
230            "gushort" => "unsigned short",
231            "gchar" => "char",
232            "guchar" => "unsigned char",
233            "gfloat" => "float",
234            "gdouble" => "double",
235            other => other,
236        }
237    }
238
239    /// Return true if `self` and `other` represent the same type, treating
240    /// GLib C aliases as equivalent to their underlying C types.
241    pub fn matches(&self, other: &Self) -> bool {
242        self.normalized_base_type() == other.normalized_base_type()
243            && self.pointer_depth == other.pointer_depth
244            && self.is_const == other.is_const
245    }
246
247    /// Returns the `BasicType` if this C type is a primitive scalar.
248    /// Handles GLib names (`gint`), C equivalents (`int`), C99 fixed-width
249    /// types (`int32_t`), and C99 types (`long long`, `_Bool`).
250    /// `gchar *` / `char *` at pointer_depth 1 maps to `BasicType::String`.
251    pub fn as_basic(&self) -> Option<BasicType> {
252        match (self.base_type.as_str(), self.pointer_depth) {
253            ("gboolean", 0) => Some(BasicType::Boolean),
254            ("gchar" | "char", 0) => Some(BasicType::Char),
255            ("guchar" | "unsigned char", 0) => Some(BasicType::UChar),
256            ("gint" | "int" | "signed" | "signed int", 0) => Some(BasicType::Int),
257            ("guint" | "unsigned int" | "unsigned", 0) => Some(BasicType::UInt),
258            ("glong" | "long" | "signed long" | "long int" | "signed long int", 0) => {
259                Some(BasicType::Long)
260            }
261            ("gulong" | "unsigned long" | "unsigned long int", 0) => Some(BasicType::ULong),
262            ("gint64" | "int64_t", 0) => Some(BasicType::Int64),
263            ("guint64" | "uint64_t", 0) => Some(BasicType::UInt64),
264            ("gfloat" | "float", 0) => Some(BasicType::Float),
265            ("gdouble" | "double", 0) => Some(BasicType::Double),
266            ("gchar" | "char", 1) => Some(BasicType::String),
267            ("gpointer" | "void", 1) => Some(BasicType::Pointer),
268            // C types without a G_TYPE_* equivalent
269            ("_Bool" | "bool", 0) => Some(BasicType::Bool),
270            ("gshort" | "short" | "signed short" | "short int" | "signed short int", 0) => {
271                Some(BasicType::Short)
272            }
273            ("gushort" | "unsigned short" | "unsigned short int", 0) => Some(BasicType::UShort),
274            ("long long" | "signed long long" | "long long int" | "signed long long int", 0) => {
275                Some(BasicType::LongLong)
276            }
277            ("unsigned long long" | "unsigned long long int", 0) => Some(BasicType::ULongLong),
278            ("long double", 0) => Some(BasicType::LongDouble),
279            ("gint8" | "int8_t" | "signed char", 0) => Some(BasicType::Int8),
280            ("guint8" | "uint8_t", 0) => Some(BasicType::UInt8),
281            ("gint16" | "int16_t", 0) => Some(BasicType::Int16),
282            ("guint16" | "uint16_t", 0) => Some(BasicType::UInt16),
283            ("gint32" | "int32_t", 0) => Some(BasicType::Int32),
284            ("guint32" | "uint32_t", 0) => Some(BasicType::UInt32),
285            ("gsize" | "size_t", 0) => Some(BasicType::Size),
286            ("gssize" | "ssize_t", 0) => Some(BasicType::SSize),
287            ("goffset", 0) => Some(BasicType::Offset),
288            ("gintptr" | "intptr_t", 0) => Some(BasicType::IntPtr),
289            ("guintptr" | "uintptr_t", 0) => Some(BasicType::UIntPtr),
290            // gconstpointer is const gpointer, maps to Pointer, is_const carries the qualifier
291            ("gconstpointer", 0) => Some(BasicType::Pointer),
292            _ => None,
293        }
294    }
295
296    pub fn is_basic(&self) -> bool {
297        matches!(
298            self.base_type.as_str(),
299            "gboolean"
300                | "gchar"
301                | "char"
302                | "guchar"
303                | "unsigned char"
304                | "gint"
305                | "int"
306                | "signed"
307                | "signed int"
308                | "guint"
309                | "unsigned int"
310                | "unsigned"
311                | "glong"
312                | "long"
313                | "signed long"
314                | "long int"
315                | "signed long int"
316                | "gulong"
317                | "unsigned long"
318                | "unsigned long int"
319                | "gint64"
320                | "int64_t"
321                | "guint64"
322                | "uint64_t"
323                | "gfloat"
324                | "float"
325                | "gdouble"
326                | "double"
327                | "gpointer"
328                | "void"
329                | "gconstpointer"
330                | "_Bool"
331                | "bool"
332                | "gshort"
333                | "short"
334                | "signed short"
335                | "short int"
336                | "signed short int"
337                | "gushort"
338                | "unsigned short"
339                | "unsigned short int"
340                | "long long"
341                | "signed long long"
342                | "long long int"
343                | "signed long long int"
344                | "unsigned long long"
345                | "unsigned long long int"
346                | "long double"
347                | "gint8"
348                | "int8_t"
349                | "signed char"
350                | "guint8"
351                | "uint8_t"
352                | "gint16"
353                | "int16_t"
354                | "guint16"
355                | "uint16_t"
356                | "gint32"
357                | "int32_t"
358                | "guint32"
359                | "uint32_t"
360                | "gsize"
361                | "size_t"
362                | "gssize"
363                | "ssize_t"
364                | "goffset"
365                | "gintptr"
366                | "intptr_t"
367                | "guintptr"
368                | "uintptr_t"
369        )
370    }
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376
377    #[test]
378    fn test_parse_type_info() {
379        let t = TypeInfo::new("g_autofree char *", SourceLocation::default());
380        assert_eq!(t.auto_cleanup, Some(AutoCleanupMacro::Autofree));
381        assert_eq!(t.base_type, "char");
382        assert_eq!(t.pointer_depth, 1);
383
384        let t = TypeInfo::new(
385            "g_autofree FuZipFirmwareWriteItem *",
386            SourceLocation::default(),
387        );
388        assert_eq!(t.auto_cleanup, Some(AutoCleanupMacro::Autofree));
389        assert_eq!(t.base_type, "FuZipFirmwareWriteItem");
390
391        let t = TypeInfo::new("const g_autofree char *", SourceLocation::default());
392        assert_eq!(t.auto_cleanup, Some(AutoCleanupMacro::Autofree));
393        assert!(t.is_const);
394        assert_eq!(t.base_type, "char");
395
396        let t = TypeInfo::new("g_autoptr(GFile)", SourceLocation::default());
397        assert_eq!(
398            t.auto_cleanup,
399            Some(AutoCleanupMacro::Autoptr("GFile".into()))
400        );
401        assert_eq!(t.base_type, "GFile");
402
403        let t = TypeInfo::new("g_autoptr (GFile)", SourceLocation::default());
404        assert_eq!(
405            t.auto_cleanup,
406            Some(AutoCleanupMacro::Autoptr("GFile".into()))
407        );
408        assert_eq!(t.base_type, "GFile");
409
410        let t = TypeInfo::new(
411            "g_autoptr (GdmConfigCommandHandler)",
412            SourceLocation::default(),
413        );
414        assert_eq!(
415            t.auto_cleanup,
416            Some(AutoCleanupMacro::Autoptr("GdmConfigCommandHandler".into()))
417        );
418        assert_eq!(t.base_type, "GdmConfigCommandHandler");
419
420        let t = TypeInfo::new("g_auto(GString)", SourceLocation::default());
421        assert_eq!(
422            t.auto_cleanup,
423            Some(AutoCleanupMacro::Auto("GString".into()))
424        );
425        assert_eq!(t.base_type, "GString");
426
427        let t = TypeInfo::new("g_auto (GString)", SourceLocation::default());
428        assert_eq!(
429            t.auto_cleanup,
430            Some(AutoCleanupMacro::Auto("GString".into()))
431        );
432        assert_eq!(t.base_type, "GString");
433
434        let t = TypeInfo::new("g_autolist(GFile)", SourceLocation::default());
435        assert_eq!(
436            t.auto_cleanup,
437            Some(AutoCleanupMacro::Autolist("GFile".into()))
438        );
439        assert_eq!(t.base_type, "GFile");
440
441        let t = TypeInfo::new("g_autolist (GFile)", SourceLocation::default());
442        assert_eq!(
443            t.auto_cleanup,
444            Some(AutoCleanupMacro::Autolist("GFile".into()))
445        );
446        assert_eq!(t.base_type, "GFile");
447
448        let t = TypeInfo::new("g_autoslist(GFile)", SourceLocation::default());
449        assert_eq!(
450            t.auto_cleanup,
451            Some(AutoCleanupMacro::Autoslist("GFile".into()))
452        );
453        assert_eq!(t.base_type, "GFile");
454
455        let t = TypeInfo::new("g_autoslist (GFile)", SourceLocation::default());
456        assert_eq!(
457            t.auto_cleanup,
458            Some(AutoCleanupMacro::Autoslist("GFile".into()))
459        );
460        assert_eq!(t.base_type, "GFile");
461
462        let t = TypeInfo::new("g_autoqueue(GFile)", SourceLocation::default());
463        assert_eq!(
464            t.auto_cleanup,
465            Some(AutoCleanupMacro::Autoqueue("GFile".into()))
466        );
467        assert_eq!(t.base_type, "GFile");
468
469        let t = TypeInfo::new("g_autoqueue (GFile)", SourceLocation::default());
470        assert_eq!(
471            t.auto_cleanup,
472            Some(AutoCleanupMacro::Autoqueue("GFile".into()))
473        );
474        assert_eq!(t.base_type, "GFile");
475
476        let t = TypeInfo::new("char *", SourceLocation::default());
477        assert_eq!(t.auto_cleanup, None);
478        assert_eq!(t.base_type, "char");
479        assert_eq!(t.pointer_depth, 1);
480    }
481}