Skip to main content

gobject_ast/parser/
mod.rs

1mod expression;
2mod gobject;
3mod statement;
4mod top_level;
5
6use std::{
7    fs,
8    path::{Path, PathBuf},
9    sync::Arc,
10};
11
12use anyhow::{Context, Result};
13use ignore::WalkBuilder;
14use tree_sitter::{Node, Parser as TSParser};
15
16use crate::model::*;
17
18pub struct Parser {
19    parser: TSParser,
20    current_file: Option<std::path::PathBuf>,
21    current_source: Arc<Vec<u8>>,
22}
23
24impl Parser {
25    pub fn new() -> Result<Self> {
26        let mut parser = TSParser::new();
27        parser
28            .set_language(&tree_sitter_c_gobject::LANGUAGE.into())
29            .context("Failed to load C grammar")?;
30
31        Ok(Self {
32            parser,
33            current_file: None,
34            current_source: Arc::new(Vec::new()),
35        })
36    }
37
38    /// Helper to create SourceLocation from a tree-sitter Node
39    fn node_location(&self, node: Node) -> SourceLocation {
40        SourceLocation::new(
41            node.start_position().row + 1,
42            node.start_position().column + 1,
43            node.start_byte(),
44            node.end_byte(),
45            Arc::clone(&self.current_source),
46        )
47    }
48
49    /// Check if a tree-sitter node is an expression
50    fn is_expression_node(node: &Node) -> bool {
51        matches!(
52            node.kind(),
53            "call_expression"
54                | "g_allocation_call"
55                | "likelihood_call"
56                | "va_arg_expression"
57                | "assignment_expression"
58                | "binary_expression"
59                | "unary_expression"
60                | "pointer_expression"
61                | "parenthesized_expression"
62                | "identifier"
63                | "field_expression"
64                | "string_literal"
65                | "number_literal"
66                | "null"
67                | "NULL"
68                | "true"
69                | "TRUE"
70                | "false"
71                | "FALSE"
72                | "cast_expression"
73                | "conditional_expression"
74                | "sizeof_expression"
75                | "alignof_expression"
76                | "subscript_expression"
77                | "initializer_list"
78                | "char_literal"
79                | "update_expression"
80                | "concatenated_string"
81                | "compound_literal_expression"
82                | "comma_expression"
83                | "offsetof_expression"
84                | "gnu_asm_expression"
85                | "compound_statement"
86                | "comment"
87                | "objc_message_expr"
88        )
89    }
90
91    pub fn parse_directory(&mut self, path: &Path) -> Result<Project> {
92        let mut project = Project::new();
93
94        // Parse all files (.h and .c)
95        // WalkBuilder respects .gitignore by default
96        for entry in WalkBuilder::new(path)
97            .hidden(false)
98            .git_ignore(true)
99            .git_global(true)
100            .git_exclude(true)
101            .require_git(false)
102            .build()
103            .filter_map(std::result::Result::ok)
104            .filter(|e| {
105                e.path()
106                    .extension()
107                    .is_some_and(|ext| ext == "h" || ext == "c")
108            })
109        {
110            let (file_path, model) = self.parse_file_to_model(entry.path())?;
111            project.files.insert(file_path, model);
112        }
113
114        project.resolve_all_gobject_types();
115        Ok(project)
116    }
117
118    pub fn parse_file(&mut self, path: &Path) -> Result<Project> {
119        let (path, model) = self.parse_file_to_model(path)?;
120        let mut project = Project::new();
121        project.files.insert(path, model);
122        project.resolve_all_gobject_types();
123        Ok(project)
124    }
125
126    pub fn parse_file_to_model(&mut self, path: &Path) -> Result<(PathBuf, FileModel)> {
127        let _file_span = tracing::warn_span!("file", path = %path.display()).entered();
128        self.current_file = Some(path.to_path_buf());
129        let source = Arc::new(fs::read(path)?);
130        self.current_source = Arc::clone(&source);
131        let tree = self
132            .parser
133            .parse(source.as_slice(), None)
134            .context("Failed to parse file")?;
135
136        let mut file_model = FileModel::new(path.to_path_buf());
137
138        self.visit_node(tree.root_node(), source.as_slice(), &mut file_model);
139
140        file_model.source = source;
141
142        Ok((path.to_path_buf(), file_model))
143    }
144
145    fn find_export_macros_in_declaration(
146        &self,
147        decl_node: Node,
148        source: &[u8],
149    ) -> Vec<ExportMacro> {
150        let mut result = Vec::new();
151        let mut cursor = decl_node.walk();
152
153        for child in decl_node.children(&mut cursor) {
154            if child.kind() == "macro_modifier" {
155                let text = std::str::from_utf8(&source[child.byte_range()]).unwrap_or("");
156                result.push(ExportMacro::parse(text.trim()));
157            }
158        }
159
160        result
161    }
162
163    /// Extract GObject type from a gobject_type_macro or macro_modifier node
164    /// The grammar now properly parses argument_list with identifier children
165    fn extract_gobject_from_macro_modifier(
166        &self,
167        node: Node,
168        source: &[u8],
169    ) -> Option<GObjectType> {
170        // Collect export macros from gobject_export_macro children
171        let export_macros: Vec<ExportMacro> = {
172            let mut cursor = node.walk();
173            node.children(&mut cursor)
174                .filter(|c| c.kind() == "gobject_export_macro")
175                .filter_map(|c| std::str::from_utf8(&source[c.byte_range()]).ok())
176                .map(|s| ExportMacro::parse(s.trim()))
177                .collect()
178        };
179
180        // Get macro name from the text (before parentheses)
181        let full_text = std::str::from_utf8(&source[node.byte_range()]).ok()?;
182        let macro_name = full_text.split('(').next()?.trim();
183        // Strip any leading export macro prefix to get the actual macro name
184        let macro_name = macro_name.split_whitespace().last()?;
185
186        let mut gobject_type =
187            self.extract_gobject_from_identifier(node, node, source, macro_name)?;
188        gobject_type.export_macros = export_macros;
189        Some(gobject_type)
190    }
191
192    fn visit_node(&self, node: Node, source: &[u8], file_model: &mut FileModel) {
193        stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
194            self.visit_node_inner(node, source, file_model);
195        });
196    }
197
198    fn visit_node_inner(&self, node: Node, source: &[u8], file_model: &mut FileModel) {
199        // Try to parse this node as a top-level item. If successful, don't
200        // recurse — children are handled inside parse_top_level_item itself
201        // (e.g. via parse_conditional_body for #ifdef blocks).
202        if let Some(item) = self.parse_top_level_item(node, source) {
203            // Doc comments are attached via prev_named_sibling(), which means
204            // the comment was already added as a standalone Comment item on the
205            // previous iteration.  Remove it so the doc only exists in one
206            // place.
207            if item.has_doc()
208                && let Some(prev) = node.prev_named_sibling()
209                && prev.kind() == "comment"
210            {
211                let prev_byte = prev.start_byte();
212                if let Some(last) = file_model.top_level_items.last()
213                    && last.is_comment_at_byte(prev_byte)
214                {
215                    file_model.top_level_items.pop();
216                }
217            }
218            file_model.top_level_items.push(item);
219            return;
220        }
221
222        // Recurse into ERROR nodes to pick up any valid top-level items
223        // that tree-sitter managed to parse inside the error region.
224        if node.kind() == "ERROR" {
225            let mut cursor = node.walk();
226            for child in node.children(&mut cursor) {
227                self.visit_node(child, source, file_model);
228            }
229            return;
230        }
231
232        // Only recurse when the node wasn't recognized as a top-level item
233        // (translation_unit, unrecognized wrapper nodes, etc.)
234        let mut cursor = node.walk();
235        for child in node.children(&mut cursor) {
236            self.visit_node(child, source, file_model);
237        }
238    }
239
240    fn extract_function_from_definition<'a>(
241        &self,
242        node: Node,
243        source: &'a [u8],
244    ) -> Option<(&'a str, bool, bool)> {
245        let func_text = std::str::from_utf8(&source[node.byte_range()]).ok()?;
246        let is_static = func_text.starts_with("static") || func_text.contains("\nstatic ");
247        let is_inline = func_text.contains("inline ");
248
249        let name = if let Some(declarator) = node.child_by_field_name("declarator") {
250            match self.extract_declarator_name(declarator, source) {
251                Some(n) if !n.is_empty() => n,
252                _ => self.extract_name_from_macro_type_specifier(node, source)?,
253            }
254        } else if let Some(func_decl) = self.find_function_declarator(node) {
255            self.extract_declarator_name(func_decl, source)?
256        } else {
257            self.extract_name_from_macro_type_specifier(node, source)?
258        };
259
260        Some((name, is_static, is_inline))
261    }
262
263    /// Extract function name from a macro_type_specifier child node.
264    /// When tree-sitter encounters an unknown return type (e.g. HWND), it
265    /// parses the function name as the first identifier inside a
266    /// macro_type_specifier node.
267    fn extract_name_from_macro_type_specifier<'a>(
268        &self,
269        node: Node,
270        source: &'a [u8],
271    ) -> Option<&'a str> {
272        let mut cursor = node.walk();
273        let macro_spec = node
274            .children(&mut cursor)
275            .find(|c| c.kind() == "macro_type_specifier")?;
276        let id = macro_spec.child(0).filter(|c| c.kind() == "identifier")?;
277        let name = std::str::from_utf8(&source[id.byte_range()]).ok()?;
278        if name.is_empty() { None } else { Some(name) }
279    }
280
281    pub(super) fn find_function_declarator<'a>(&self, node: Node<'a>) -> Option<Node<'a>> {
282        if node.kind() == "function_declarator" {
283            return Some(node);
284        }
285
286        // For pointer/abstract declarators, look in the declarator field
287        if let Some(declarator) = node.child_by_field_name("declarator")
288            && let Some(found) = self.find_function_declarator(declarator)
289        {
290            return Some(found);
291        }
292
293        // Recursively search children
294        let mut cursor = node.walk();
295        for child in node.children(&mut cursor) {
296            if let Some(found) = self.find_function_declarator(child) {
297                return Some(found);
298            }
299        }
300
301        None
302    }
303
304    pub(super) fn extract_comment_text(
305        &self,
306        node: Node,
307        source: &[u8],
308    ) -> Option<(CommentKind, String)> {
309        let text = std::str::from_utf8(&source[node.byte_range()]).ok()?;
310
311        if text.starts_with("//") {
312            Some((CommentKind::Line, text.to_string()))
313        } else if text.starts_with("/*") && text.ends_with("*/") {
314            Some((CommentKind::Block, text.to_string()))
315        } else {
316            None
317        }
318    }
319}
320
321impl Default for Parser {
322    fn default() -> Self {
323        Self::new().expect("Failed to create parser")
324    }
325}