Skip to main content

gobject_ast/model/
project.rs

1use std::{collections::HashMap, path::PathBuf};
2
3use serde::Serialize;
4
5use crate::{
6    Comment, GObjectType, SourceLocation, Statement, TypeInfo, VariableDecl,
7    model::{
8        doc::TypeDoc,
9        expression::Expression,
10        top_level::{FunctionDefItem, TopLevelItem},
11        types::EnumInfo,
12    },
13    top_level::{FunctionDeclItem, PreprocessorDirective, TypeDefItem, TypedefTarget},
14};
15
16/// The complete project model - a map of files to their content
17#[derive(Debug, Clone, Serialize, Default)]
18pub struct Project {
19    #[serde(skip_serializing_if = "HashMap::is_empty")]
20    pub files: HashMap<PathBuf, FileModel>,
21}
22
23impl Project {
24    pub fn new() -> Self {
25        Self {
26            files: HashMap::new(),
27        }
28    }
29
30    /// Get a file's model
31    pub fn get_file(&self, path: &PathBuf) -> Option<&FileModel> {
32        self.files.get(path)
33    }
34
35    /// Check if a function is declared in any header
36    pub fn is_function_declared_in_header(&self, name: &str) -> bool {
37        for file in self.files.values() {
38            if file.path.extension().is_some_and(|ext| ext == "h")
39                && file
40                    .iter_function_declarations()
41                    .any(|decl| decl.name == name)
42            {
43                return true;
44            }
45        }
46        false
47    }
48
49    /// Check if a function has export macros (truly public API)
50    pub fn is_function_exported(&self, name: &str) -> bool {
51        for file in self.files.values() {
52            if file
53                .iter_function_declarations()
54                .any(|decl| decl.name == name && !decl.export_macros.is_empty())
55            {
56                return true;
57            }
58        }
59        false
60    }
61}
62
63/// Model of a single file (header or C file)
64#[derive(Debug, Clone, Serialize)]
65pub struct FileModel {
66    pub path: PathBuf,
67    /// Top-level items in source order (preserves structure like #ifdef blocks)
68    #[serde(skip_serializing_if = "Vec::is_empty")]
69    pub top_level_items: Vec<TopLevelItem>,
70    /// The raw source code of this file - available for detailed pattern
71    /// matching
72    #[serde(skip)]
73    pub source: Vec<u8>,
74}
75
76impl FileModel {
77    pub fn new(path: PathBuf) -> Self {
78        Self {
79            path,
80            top_level_items: Vec::new(),
81            source: Vec::new(),
82        }
83    }
84
85    /// Iterate through all includes in the file (including those in #ifdef
86    /// blocks)
87    pub fn iter_all_includes(&self) -> impl Iterator<Item = (&str, bool, SourceLocation)> + '_ {
88        self.iter_items_recursive(&self.top_level_items)
89            .filter_map(|item| match item {
90                TopLevelItem::Preprocessor(PreprocessorDirective::Include {
91                    path,
92                    is_system,
93                    location,
94                }) => Some((path.as_str(), *is_system, *location)),
95                _ => None,
96            })
97    }
98
99    /// Iterate through all function definitions in the file (including those in
100    /// #ifdef blocks)
101    pub fn iter_function_definitions(&self) -> impl Iterator<Item = &FunctionDefItem> + '_ {
102        self.iter_items_recursive(&self.top_level_items)
103            .filter_map(|item| match item {
104                TopLevelItem::FunctionDefinition(func) => Some(func),
105                _ => None,
106            })
107    }
108
109    /// Iterate through class_init functions
110    pub fn iter_class_init_functions(&self) -> impl Iterator<Item = &FunctionDefItem> + '_ {
111        self.iter_function_definitions()
112            .filter(|f| f.name.ends_with("_class_init"))
113    }
114
115    /// Iterate through all function declarations in the file (including those
116    /// in #ifdef blocks)
117    pub fn iter_function_declarations(&self) -> impl Iterator<Item = &FunctionDeclItem> + '_ {
118        self.iter_items_recursive(&self.top_level_items)
119            .filter_map(|item| match item {
120                TopLevelItem::FunctionDeclaration(func) => Some(func),
121                _ => None,
122            })
123    }
124
125    /// Iterate through all functions (both declarations and definitions),
126    /// returning function names
127    pub fn iter_all_function_names(&self) -> impl Iterator<Item = &str> + '_ {
128        self.iter_items_recursive(&self.top_level_items)
129            .filter_map(|item| match item {
130                TopLevelItem::FunctionDefinition(func) => Some(func.name.as_str()),
131                TopLevelItem::FunctionDeclaration(func) => Some(func.name.as_str()),
132                _ => None,
133            })
134    }
135
136    /// Iterate through all GObject type declarations (including those in #ifdef
137    /// blocks)
138    pub fn iter_all_gobject_types(&self) -> impl Iterator<Item = &GObjectType> + '_ {
139        self.iter_items_recursive(&self.top_level_items)
140            .filter_map(|item| match item {
141                TopLevelItem::Preprocessor(PreprocessorDirective::GObjectType(gobject_type)) => {
142                    Some(gobject_type.as_ref())
143                }
144                _ => None,
145            })
146    }
147
148    /// Find the class or interface struct for a given `GObjectType`.
149    /// Returns `None` for final types (no class struct) and when the struct
150    /// was not found in this file.
151    pub fn find_class_struct_for(&self, gobject_type: &GObjectType) -> Option<&TypeDefItem> {
152        let name = gobject_type.class_struct_name()?;
153        self.iter_class_structs().find(|td| {
154            if let TypeDefItem::Struct { name: n, .. } = td {
155                n.trim_start_matches('_') == name
156            } else {
157                false
158            }
159        })
160    }
161
162    /// Iterate through all class structs (structs ending with `Class` or
163    /// `Interface` that have at least one vfunc).
164    pub fn iter_class_structs(&self) -> impl Iterator<Item = &TypeDefItem> + '_ {
165        self.iter_items_recursive(&self.top_level_items)
166            .filter_map(|item| match item {
167                TopLevelItem::TypeDefinition(td @ TypeDefItem::Struct { vfuncs, .. })
168                    if !vfuncs.is_empty() =>
169                {
170                    Some(td)
171                }
172                _ => None,
173            })
174    }
175
176    /// Iterate through all standalone comments (including those in #ifdef
177    /// blocks)
178    pub fn iter_comments(&self) -> impl Iterator<Item = &Comment> + '_ {
179        self.iter_items_recursive(&self.top_level_items)
180            .filter_map(|item| match item {
181                TopLevelItem::Comment(c) => Some(c),
182                _ => None,
183            })
184    }
185
186    /// Iterate through all enum definitions (including those in #ifdef blocks)
187    pub fn iter_all_enums(&self) -> impl Iterator<Item = &EnumInfo> + '_ {
188        self.iter_items_recursive(&self.top_level_items)
189            .filter_map(|item| match item {
190                TopLevelItem::TypeDefinition(TypeDefItem::Enum(enum_info)) => {
191                    Some(enum_info.as_ref())
192                }
193                _ => None,
194            })
195    }
196
197    /// Iterate through property enums (enums that appear to define GObject
198    /// properties) Filters for enums where first member starts with PROP_
199    /// or ends with _PROP_0
200    pub fn iter_property_enums(&self) -> impl Iterator<Item = &EnumInfo> + '_ {
201        self.iter_all_enums().filter(|e| e.is_property_enum())
202    }
203
204    /// Find the GObjectType whose class_init installs signals from the given
205    /// signal enum.
206    pub fn find_gobject_type_for_signal_enum(&self, enum_info: &EnumInfo) -> Option<&GObjectType> {
207        let signal_names: Vec<&str> = enum_info
208            .values
209            .iter()
210            .filter(|v| !v.is_signal_last())
211            .map(|v| v.name.as_str())
212            .collect();
213
214        let n_signals_name = enum_info
215            .values
216            .last()
217            .filter(|v| v.is_signal_last())
218            .map(|v| v.name.as_str());
219
220        let arrays = self.find_typed_arrays("guint", false, n_signals_name);
221        let array_names: Vec<&str> = arrays.iter().map(|d| d.name.as_str()).collect();
222
223        self.iter_all_gobject_types().find(|gt| {
224            let class_init_name = gt.class_init_function_name();
225            let Some(func) = self
226                .iter_function_definitions()
227                .find(|f| f.name == class_init_name)
228            else {
229                return false;
230            };
231
232            // Check if class_init assigns to the signal array
233            if !array_names.is_empty() {
234                let uses_array = func
235                    .body_statements
236                    .iter()
237                    .flat_map(Statement::iter_assignments)
238                    .any(|a| {
239                        matches!(&*a.lhs, Expression::Subscript(sub)
240                            if matches!(&*sub.array, Expression::Identifier(id)
241                                if array_names.contains(&id.name.as_str())))
242                    });
243                if uses_array {
244                    return true;
245                }
246            }
247
248            // Check if class_init uses signal enum values in subscript assignments
249            // with g_signal_new
250            func.body_statements
251                .iter()
252                .flat_map(Statement::iter_assignments)
253                .any(|a| {
254                    if let Expression::Subscript(sub) = &*a.lhs
255                        && let Expression::Identifier(index_id) = &*sub.index
256                        && signal_names.contains(&index_id.name.as_str())
257                        && let Expression::Call(call) = &*a.rhs
258                        && call.function_contains("g_signal_new", &self.source)
259                    {
260                        true
261                    } else {
262                        false
263                    }
264                })
265        })
266    }
267
268    /// Find array declarations of a specific type, optionally filtered by
269    /// sentinel
270    ///
271    /// If sentinel_name is None, returns ALL arrays of that type.
272    ///
273    /// Examples:
274    /// - `find_typed_arrays("GParamSpec", true, Some("N_PROPS"))` finds
275    ///   `GParamSpec *props[N_PROPS]`
276    /// - `find_typed_arrays("GParamSpec", true, None)` finds ALL `GParamSpec
277    ///   *[]` arrays
278    /// - `find_typed_arrays("guint", false, Some("N_SIGNALS"))` finds `guint
279    ///   signals[N_SIGNALS]`
280    pub fn find_typed_arrays(
281        &self,
282        base_type: &str,
283        is_pointer: bool,
284        sentinel_name: Option<&str>,
285    ) -> Vec<&VariableDecl> {
286        self.iter_all_items()
287            .filter_map(|item| {
288                let TopLevelItem::Declaration(decl) = item else {
289                    return None;
290                };
291                if !decl.type_info.is_base_type(base_type)
292                    || decl.type_info.is_pointer() != is_pointer
293                {
294                    return None;
295                }
296                let matches = match &decl.array_size {
297                    Some(Expression::Identifier(size_id)) => {
298                        sentinel_name.is_none_or(|s| size_id.name == s)
299                    }
300                    Some(Expression::Binary(_)) => sentinel_name.is_none(),
301                    Some(_) => sentinel_name.is_none(),
302                    None => false,
303                };
304                matches.then_some(decl.as_ref())
305            })
306            .collect()
307    }
308
309    /// Find the GObjectType whose properties match the given property enum.
310    pub fn find_gobject_type_for_property_enum(
311        &self,
312        enum_info: &EnumInfo,
313    ) -> Option<&GObjectType> {
314        let property_names: Vec<&str> = enum_info
315            .values
316            .iter()
317            .filter(|v| !v.is_prop_0() && !v.is_prop_last())
318            .map(|v| v.name.as_str())
319            .collect();
320
321        let n_props_name = enum_info
322            .values
323            .last()
324            .filter(|v| v.is_prop_last())
325            .map(|v| v.name.as_str());
326
327        self.iter_all_gobject_types().find(|gt| {
328            // Match by property enum values in assignments
329            let has_matching_property = gt.properties.iter().any(|a| {
330                a.get_installed_enum_value(&self.source)
331                    .is_some_and(|ev| property_names.contains(&ev))
332            });
333            if has_matching_property {
334                return true;
335            }
336
337            // Match by N_PROPS sentinel used in GParamSpec arrays referenced
338            // from this GObjectType's class_init
339            if let Some(sentinel) = n_props_name {
340                let class_init_name = gt.class_init_function_name();
341                if let Some(func) = self
342                    .iter_function_definitions()
343                    .find(|f| f.name == class_init_name)
344                {
345                    let install_calls = func.find_install_properties_calls();
346                    return install_calls.iter().any(|call| {
347                        call.get_arg(1)
348                            .and_then(|arg| arg.to_source_string(&self.source))
349                            .is_some_and(|name| name == sentinel)
350                    });
351                }
352            }
353
354            false
355        })
356    }
357
358    /// Iterate through all top-level items recursively (including items inside
359    /// `#ifdef`/`#if` and `G_BEGIN_DECLS` blocks). Conditional container items
360    /// themselves are also yielded before their children.
361    pub fn iter_all_items(&self) -> impl Iterator<Item = &TopLevelItem> + '_ {
362        self.iter_items_recursive(&self.top_level_items)
363    }
364
365    /// Iterate typedef forward-alias declarations of the form
366    /// `typedef [struct|union] _Foo Foo` (i.e., typedefs that have no inline
367    /// struct body). Yields `(typedef_name, target_TypeInfo)` so callers can
368    /// inspect `target_type.base_type`, `.is_struct`, and `.is_union`.
369    pub fn iter_typedef_pairs(&self) -> impl Iterator<Item = (&str, &TypeInfo)> + '_ {
370        self.iter_all_items().filter_map(|item| match item {
371            TopLevelItem::TypeDefinition(TypeDefItem::Typedef {
372                name,
373                target: TypedefTarget::Type(target_type),
374                struct_fields,
375                ..
376            }) if struct_fields.is_empty() && !target_type.base_type.is_empty() => {
377                Some((name.as_str(), target_type))
378            }
379            _ => None,
380        })
381    }
382
383    /// Populate `properties` and `signals` on each `GObjectType` by finding
384    /// the matching `*_class_init` function and extracting param_spec
385    /// assignments and signal registrations from it.
386    pub fn resolve_gobject_types(&mut self) {
387        Self::resolve_items(&mut self.top_level_items, &self.source);
388    }
389
390    fn resolve_items(items: &mut [TopLevelItem], source: &[u8]) {
391        for i in 0..items.len() {
392            match &items[i] {
393                TopLevelItem::Preprocessor(PreprocessorDirective::GObjectType(gt)) => {
394                    let class_init_name = gt.class_init_function_name();
395                    let type_name = gt.type_name.clone();
396                    let func_idx = items.iter().position(|item| {
397                        matches!(item, TopLevelItem::FunctionDefinition(f) if f.name == class_init_name)
398                    });
399                    if let Some(j) = func_idx {
400                        let func = match &items[j] {
401                            TopLevelItem::FunctionDefinition(f) => f,
402                            _ => unreachable!(),
403                        };
404
405                        let properties = func.find_param_spec_assignments(source);
406                        let signals = func.find_signal_registrations(source);
407                        if let TopLevelItem::Preprocessor(PreprocessorDirective::GObjectType(gt)) =
408                            &mut items[i]
409                        {
410                            gt.properties = properties;
411                            gt.signals = signals;
412                        }
413                    }
414
415                    let doc = items.iter().find_map(|item| {
416                        if let TopLevelItem::Comment(c) = item {
417                            let td = TypeDoc::from_comment(c)?;
418                            if td.symbol.as_deref() == Some(type_name.as_str()) {
419                                return Some(td);
420                            }
421                        }
422                        None
423                    });
424                    if let Some(doc) = doc
425                        && let TopLevelItem::Preprocessor(PreprocessorDirective::GObjectType(gt)) =
426                            &mut items[i]
427                    {
428                        gt.doc = Some(doc);
429                    }
430                }
431                TopLevelItem::Preprocessor(PreprocessorDirective::Conditional { .. })
432                | TopLevelItem::Preprocessor(PreprocessorDirective::GObjectDeclsBlock { .. }) => {}
433                _ => {}
434            }
435        }
436
437        for item in items.iter_mut() {
438            match item {
439                TopLevelItem::Preprocessor(PreprocessorDirective::Conditional { body, .. })
440                | TopLevelItem::Preprocessor(PreprocessorDirective::GObjectDeclsBlock {
441                    body,
442                    ..
443                }) => {
444                    Self::resolve_items(body, source);
445                }
446                _ => {}
447            }
448        }
449    }
450
451    /// Recursively iterate through all items (including those in #ifdef blocks)
452    fn iter_items_recursive<'a>(
453        &'a self,
454        items: &'a [TopLevelItem],
455    ) -> Box<dyn Iterator<Item = &'a TopLevelItem> + 'a> {
456        Box::new(items.iter().flat_map(move |item| match item {
457            TopLevelItem::Preprocessor(PreprocessorDirective::Conditional { body, .. })
458            | TopLevelItem::Preprocessor(PreprocessorDirective::GObjectDeclsBlock {
459                body, ..
460            }) => Box::new(std::iter::once(item).chain(self.iter_items_recursive(body)))
461                as Box<dyn Iterator<Item = &'a TopLevelItem>>,
462            _ => Box::new(std::iter::once(item)) as Box<dyn Iterator<Item = &'a TopLevelItem>>,
463        }))
464    }
465}