Skip to main content

normalize_languages/
meson.rs

1//! Meson build system support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8/// Meson language support.
9pub struct Meson;
10
11impl Language for Meson {
12    fn name(&self) -> &'static str {
13        "Meson"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["meson.build", "meson_options.txt"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "meson"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &[] // Meson doesn't have traditional containers
28    }
29
30    fn function_kinds(&self) -> &'static [&'static str] {
31        &["normal_command"] // function calls
32    }
33
34    fn type_kinds(&self) -> &'static [&'static str] {
35        &[]
36    }
37
38    fn import_kinds(&self) -> &'static [&'static str] {
39        &["normal_command"] // subproject(), dependency() are function calls
40    }
41
42    fn public_symbol_kinds(&self) -> &'static [&'static str] {
43        &["expression_statement"]
44    }
45
46    fn visibility_mechanism(&self) -> VisibilityMechanism {
47        VisibilityMechanism::AllPublic
48    }
49
50    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51        if node.kind() == "expression_statement" {
52            if let Some(name) = self.node_name(node, content) {
53                return vec![Export {
54                    name: name.to_string(),
55                    kind: SymbolKind::Variable,
56                    line: node.start_position().row + 1,
57                }];
58            }
59        }
60        Vec::new()
61    }
62
63    fn scope_creating_kinds(&self) -> &'static [&'static str] {
64        &["if_command", "foreach_command"]
65    }
66
67    fn control_flow_kinds(&self) -> &'static [&'static str] {
68        &["if_command", "foreach_command"]
69    }
70
71    fn complexity_nodes(&self) -> &'static [&'static str] {
72        &["if_command", "foreach_command", "if_condition"]
73    }
74
75    fn nesting_nodes(&self) -> &'static [&'static str] {
76        &["if_command", "foreach_command"]
77    }
78
79    fn signature_suffix(&self) -> &'static str {
80        ""
81    }
82
83    fn extract_function(
84        &self,
85        _node: &Node,
86        _content: &str,
87        _in_container: bool,
88    ) -> Option<Symbol> {
89        None // Meson uses function calls, not definitions
90    }
91
92    fn extract_container(&self, _node: &Node, _content: &str) -> Option<Symbol> {
93        None // Meson doesn't have containers
94    }
95
96    fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
97        None
98    }
99    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
100        None
101    }
102
103    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
104        Vec::new()
105    }
106
107    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
108        if node.kind() != "normal_command" {
109            return Vec::new();
110        }
111
112        let text = &content[node.byte_range()];
113        if text.starts_with("subproject(") || text.starts_with("dependency(") {
114            return vec![Import {
115                module: text.to_string(),
116                names: Vec::new(),
117                alias: None,
118                is_wildcard: false,
119                is_relative: false,
120                line: node.start_position().row + 1,
121            }];
122        }
123
124        Vec::new()
125    }
126
127    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
128        // Meson: subdir('path')
129        format!("subdir('{}')", import.module)
130    }
131
132    fn is_public(&self, _node: &Node, _content: &str) -> bool {
133        true
134    }
135    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
136        Visibility::Public
137    }
138
139    fn is_test_symbol(&self, _symbol: &crate::Symbol) -> bool {
140        false
141    }
142
143    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
144        None
145    }
146
147    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
148        node.child_by_field_name("body")
149    }
150
151    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
152        false
153    }
154
155    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
156        if let Some(name_node) = node.child_by_field_name("name") {
157            return Some(&content[name_node.byte_range()]);
158        }
159        if let Some(left_node) = node.child_by_field_name("left") {
160            return Some(&content[left_node.byte_range()]);
161        }
162        let mut cursor = node.walk();
163        for child in node.children(&mut cursor) {
164            if child.kind() == "identifier" {
165                return Some(&content[child.byte_range()]);
166            }
167        }
168        None
169    }
170
171    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
172        let name = path.file_name()?.to_str()?;
173        if name == "meson.build" || name == "meson_options.txt" {
174            Some(name.to_string())
175        } else {
176            None
177        }
178    }
179
180    fn module_name_to_paths(&self, _module: &str) -> Vec<String> {
181        vec!["meson.build".to_string()]
182    }
183
184    fn lang_key(&self) -> &'static str {
185        "meson"
186    }
187
188    fn is_stdlib_import(&self, _: &str, _: &Path) -> bool {
189        false
190    }
191    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
192        None
193    }
194    fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
195        None
196    }
197    fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
198        None
199    }
200    fn get_version(&self, _: &Path) -> Option<String> {
201        None
202    }
203    fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
204        None
205    }
206    fn indexable_extensions(&self) -> &'static [&'static str] {
207        &[]
208    } // Special filenames only
209    fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
210        Vec::new()
211    }
212
213    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
214        use crate::traits::skip_dotfiles;
215        if skip_dotfiles(name) {
216            return true;
217        }
218        !is_dir && name != "meson.build" && name != "meson_options.txt"
219    }
220
221    fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
222        Vec::new()
223    }
224
225    fn package_module_name(&self, entry_name: &str) -> String {
226        entry_name.to_string()
227    }
228
229    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
230        if path.is_file() {
231            Some(path.to_path_buf())
232        } else {
233            None
234        }
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::validate_unused_kinds_audit;
242
243    #[test]
244    fn unused_node_kinds_audit() {
245        #[rustfmt::skip]
246        let documented_unused: &[&str] = &[
247            // Control flow commands
248            "else_command", "elseif_command",
249            // Expression-related
250            "formatunit", "identifier", "operatorunit", "ternaryoperator",
251        ];
252        validate_unused_kinds_audit(&Meson, documented_unused)
253            .expect("Meson unused node kinds audit failed");
254    }
255}