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