Skip to main content

gobject_linter/
ast_context.rs

1use std::{
2    path::Path,
3    sync::atomic::{AtomicUsize, Ordering},
4};
5
6use anyhow::Result;
7use globset::GlobSet;
8use gobject_ast::{
9    Parser,
10    model::{FileModel, Project},
11};
12use ignore::WalkBuilder;
13use indicatif::ProgressBar;
14use rayon::prelude::*;
15
16use crate::{meson::MesonIntrospection, type_alias_map::TypeAliasMap};
17
18/// AST-based project context that replaces the old tree-sitter based
19/// ProjectContext
20pub struct AstContext {
21    pub project: Project,
22    /// Meson introspection data.
23    /// None means no meson info was available
24    pub meson_introspection: Option<MesonIntrospection>,
25    type_aliases: TypeAliasMap,
26}
27
28impl AstContext {
29    /// Build with ignore patterns and optional meson introspection
30    pub fn build_with_ignore(
31        directory: &Path,
32        ignore_matcher: &GlobSet,
33        spinner: Option<&ProgressBar>,
34        meson_introspection: Option<MesonIntrospection>,
35    ) -> Result<Self> {
36        // Collect all files first to get count
37        // WalkBuilder respects .gitignore, .ignore, and other ignore files
38        // automatically
39        let files: Vec<_> = WalkBuilder::new(directory)
40            .hidden(false) // Include hidden files/dirs
41            .git_ignore(true) // Respect .gitignore
42            .git_global(true) // Respect global gitignore
43            .git_exclude(true) // Respect .git/info/exclude
44            .require_git(false) // Work in non-git directories too
45            .build()
46            .filter_map(std::result::Result::ok)
47            .filter(|e| {
48                e.path()
49                    .extension()
50                    .is_some_and(|ext| ext == "h" || ext == "c")
51            })
52            .filter(|e| {
53                let path = e.path();
54                let relative_path = path.strip_prefix(directory).unwrap_or(path);
55                !ignore_matcher.is_match(relative_path)
56            })
57            .collect();
58
59        let total_files = files.len();
60        let counter = AtomicUsize::new(0);
61
62        // Parse files in parallel
63        let chunks: Vec<Vec<_>> = files
64            .par_iter()
65            .fold(
66                || (Parser::new().unwrap(), Vec::new()),
67                |(mut parser, mut results), entry| {
68                    let i = counter.fetch_add(1, Ordering::Relaxed);
69                    if let Some(sp) = spinner {
70                        sp.set_message(format!("Parsing files... {}/{}", i + 1, total_files));
71                    }
72                    if let Ok(item) = parser.parse_file_to_model(entry.path()) {
73                        results.push(item);
74                    }
75                    (parser, results)
76                },
77            )
78            .map(|(_, results)| results)
79            .collect();
80
81        let mut project = Project::new();
82        project.files.reserve(total_files);
83        for chunk in chunks {
84            for (path, model) in chunk {
85                project.files.insert(path, model);
86            }
87        }
88
89        project.resolve_all_gobject_types();
90        let type_aliases = TypeAliasMap::build(&project);
91
92        Ok(Self {
93            project,
94            meson_introspection,
95            type_aliases,
96        })
97    }
98
99    /// Update a single file in the project
100    pub fn update_file(&mut self, file_path: &Path) -> Result<()> {
101        let mut parser = Parser::new()?;
102
103        if let Ok((path, model)) = parser.parse_file_to_model(file_path) {
104            self.project.files.insert(path, model);
105        } else {
106            self.project.files.remove(file_path);
107        }
108
109        self.project.resolve_all_gobject_types();
110        self.type_aliases = TypeAliasMap::build(&self.project);
111
112        Ok(())
113    }
114
115    /// Typedef/struct-tag alias map for the whole project.
116    pub fn type_aliases(&self) -> &TypeAliasMap {
117        &self.type_aliases
118    }
119
120    pub fn iter_all_files(&self) -> impl Iterator<Item = (&Path, &FileModel)> {
121        self.project.iter_all_files()
122    }
123
124    pub fn iter_c_files(&self) -> impl Iterator<Item = (&Path, &FileModel)> {
125        self.project.iter_c_files()
126    }
127
128    pub fn iter_header_files(&self) -> impl Iterator<Item = (&Path, &FileModel)> {
129        self.project.iter_header_files()
130    }
131
132    pub fn iter_private_files(&self) -> impl Iterator<Item = (&Path, &FileModel)> {
133        self.project.files.iter().filter_map(|(path, file)| {
134            let p = path.as_path();
135            if path.extension().is_some_and(|ext| ext == "c") {
136                return Some((p, file));
137            }
138            if self.is_public_header(p) == Some(true) {
139                return None;
140            }
141            Some((p, file))
142        })
143    }
144
145    pub fn is_public_header(&self, path: &Path) -> Option<bool> {
146        let m = self.meson_introspection.as_ref()?;
147        let gir = m.get_introspected_headers();
148        let installed = m.get_installed_headers();
149        Some(gir.contains(path) || installed.contains(path))
150    }
151
152    pub fn is_gir_header(&self, path: &Path) -> Option<bool> {
153        let m = self.meson_introspection.as_ref()?;
154        let gir = m.get_introspected_headers();
155        Some(gir.contains(path))
156    }
157
158    pub fn has_public_private_info(&self) -> bool {
159        self.meson_introspection.is_some()
160    }
161
162    pub fn find_func_doc(&self, name: &str) -> Option<&gobject_ast::model::FunctionDoc> {
163        self.project.find_func_doc(name)
164    }
165
166    pub fn find_type_doc(&self, type_name: &str) -> Option<&gobject_ast::model::TypeDoc> {
167        self.project.find_type_doc(type_name)
168    }
169
170    pub fn find_gobject_type_by_gtype(
171        &self,
172        gtype: &gobject_ast::model::GType,
173    ) -> Option<&gobject_ast::model::GObjectType> {
174        self.project.find_gobject_type_by_gtype(gtype)
175    }
176}