mod expression;
mod gobject;
mod statement;
mod top_level;
use std::{
fs,
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::{Context, Result};
use ignore::WalkBuilder;
use tree_sitter::{Node, Parser as TSParser};
use crate::model::*;
pub struct Parser {
parser: TSParser,
current_file: Option<std::path::PathBuf>,
current_source: Arc<Vec<u8>>,
}
impl Parser {
pub fn new() -> Result<Self> {
let mut parser = TSParser::new();
parser
.set_language(&tree_sitter_c_gobject::LANGUAGE.into())
.context("Failed to load C grammar")?;
Ok(Self {
parser,
current_file: None,
current_source: Arc::new(Vec::new()),
})
}
fn node_location(&self, node: Node) -> SourceLocation {
SourceLocation::new(
node.start_position().row + 1,
node.start_position().column + 1,
node.start_byte(),
node.end_byte(),
Arc::clone(&self.current_source),
)
}
fn is_expression_node(node: &Node) -> bool {
matches!(
node.kind(),
"call_expression"
| "g_allocation_call"
| "likelihood_call"
| "va_arg_expression"
| "assignment_expression"
| "binary_expression"
| "unary_expression"
| "pointer_expression"
| "parenthesized_expression"
| "identifier"
| "field_expression"
| "string_literal"
| "number_literal"
| "null"
| "NULL"
| "true"
| "TRUE"
| "false"
| "FALSE"
| "cast_expression"
| "conditional_expression"
| "sizeof_expression"
| "alignof_expression"
| "subscript_expression"
| "initializer_list"
| "char_literal"
| "update_expression"
| "concatenated_string"
| "compound_literal_expression"
| "comma_expression"
| "offsetof_expression"
| "gnu_asm_expression"
| "compound_statement"
| "comment"
| "objc_message_expr"
)
}
pub fn parse_directory(&mut self, path: &Path) -> Result<Project> {
let mut project = Project::new();
for entry in WalkBuilder::new(path)
.hidden(false)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.require_git(false)
.build()
.filter_map(std::result::Result::ok)
.filter(|e| {
e.path()
.extension()
.is_some_and(|ext| ext == "h" || ext == "c")
})
{
let (file_path, model) = self.parse_file_to_model(entry.path())?;
project.files.insert(file_path, model);
}
project.resolve_all_gobject_types();
Ok(project)
}
pub fn parse_file(&mut self, path: &Path) -> Result<Project> {
let (path, model) = self.parse_file_to_model(path)?;
let mut project = Project::new();
project.files.insert(path, model);
project.resolve_all_gobject_types();
Ok(project)
}
pub fn parse_file_to_model(&mut self, path: &Path) -> Result<(PathBuf, FileModel)> {
let _file_span = tracing::warn_span!("file", path = %path.display()).entered();
self.current_file = Some(path.to_path_buf());
let source = Arc::new(fs::read(path)?);
self.current_source = Arc::clone(&source);
let tree = self
.parser
.parse(source.as_slice(), None)
.context("Failed to parse file")?;
let mut file_model = FileModel::new(path.to_path_buf());
self.visit_node(tree.root_node(), source.as_slice(), &mut file_model);
file_model.source = source;
Ok((path.to_path_buf(), file_model))
}
fn find_export_macros_in_declaration(
&self,
decl_node: Node,
source: &[u8],
) -> Vec<ExportMacro> {
let mut result = Vec::new();
let mut cursor = decl_node.walk();
for child in decl_node.children(&mut cursor) {
if child.kind() == "macro_modifier" {
let text = std::str::from_utf8(&source[child.byte_range()]).unwrap_or("");
result.push(ExportMacro::parse(text.trim()));
}
}
result
}
fn extract_gobject_from_macro_modifier(
&self,
node: Node,
source: &[u8],
) -> Option<GObjectType> {
let export_macros: Vec<ExportMacro> = {
let mut cursor = node.walk();
node.children(&mut cursor)
.filter(|c| c.kind() == "gobject_export_macro")
.filter_map(|c| std::str::from_utf8(&source[c.byte_range()]).ok())
.map(|s| ExportMacro::parse(s.trim()))
.collect()
};
let full_text = std::str::from_utf8(&source[node.byte_range()]).ok()?;
let macro_name = full_text.split('(').next()?.trim();
let macro_name = macro_name.split_whitespace().last()?;
let mut gobject_type =
self.extract_gobject_from_identifier(node, node, source, macro_name)?;
gobject_type.export_macros = export_macros;
Some(gobject_type)
}
fn visit_node(&self, node: Node, source: &[u8], file_model: &mut FileModel) {
stacker::maybe_grow(32 * 1024, 1024 * 1024, || {
self.visit_node_inner(node, source, file_model);
});
}
fn visit_node_inner(&self, node: Node, source: &[u8], file_model: &mut FileModel) {
if let Some(item) = self.parse_top_level_item(node, source) {
if item.has_doc()
&& let Some(prev) = node.prev_named_sibling()
&& prev.kind() == "comment"
{
let prev_byte = prev.start_byte();
if let Some(last) = file_model.top_level_items.last()
&& last.is_comment_at_byte(prev_byte)
{
file_model.top_level_items.pop();
}
}
file_model.top_level_items.push(item);
return;
}
if node.kind() == "ERROR" {
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
self.visit_node(child, source, file_model);
}
return;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
self.visit_node(child, source, file_model);
}
}
fn extract_function_from_definition<'a>(
&self,
node: Node,
source: &'a [u8],
) -> Option<(&'a str, bool, bool)> {
let func_text = std::str::from_utf8(&source[node.byte_range()]).ok()?;
let is_static = func_text.starts_with("static") || func_text.contains("\nstatic ");
let is_inline = func_text.contains("inline ");
let name = if let Some(declarator) = node.child_by_field_name("declarator") {
match self.extract_declarator_name(declarator, source) {
Some(n) if !n.is_empty() => n,
_ => self.extract_name_from_macro_type_specifier(node, source)?,
}
} else if let Some(func_decl) = self.find_function_declarator(node) {
self.extract_declarator_name(func_decl, source)?
} else {
self.extract_name_from_macro_type_specifier(node, source)?
};
Some((name, is_static, is_inline))
}
fn extract_name_from_macro_type_specifier<'a>(
&self,
node: Node,
source: &'a [u8],
) -> Option<&'a str> {
let mut cursor = node.walk();
let macro_spec = node
.children(&mut cursor)
.find(|c| c.kind() == "macro_type_specifier")?;
let id = macro_spec.child(0).filter(|c| c.kind() == "identifier")?;
let name = std::str::from_utf8(&source[id.byte_range()]).ok()?;
if name.is_empty() { None } else { Some(name) }
}
pub(super) fn find_function_declarator<'a>(&self, node: Node<'a>) -> Option<Node<'a>> {
if node.kind() == "function_declarator" {
return Some(node);
}
if let Some(declarator) = node.child_by_field_name("declarator")
&& let Some(found) = self.find_function_declarator(declarator)
{
return Some(found);
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if let Some(found) = self.find_function_declarator(child) {
return Some(found);
}
}
None
}
pub(super) fn extract_comment_text(
&self,
node: Node,
source: &[u8],
) -> Option<(CommentKind, String)> {
let text = std::str::from_utf8(&source[node.byte_range()]).ok()?;
if text.starts_with("//") {
Some((CommentKind::Line, text.to_string()))
} else if text.starts_with("/*") && text.ends_with("*/") {
Some((CommentKind::Block, text.to_string()))
} else {
None
}
}
}
impl Default for Parser {
fn default() -> Self {
Self::new().expect("Failed to create parser")
}
}