Skip to main content

normalize_languages/
cmake.rs

1//! CMake language support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{
5    Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism,
6    simple_function_symbol,
7};
8use std::path::{Path, PathBuf};
9use tree_sitter::Node;
10
11/// CMake language support.
12pub struct CMake;
13
14impl Language for CMake {
15    fn name(&self) -> &'static str {
16        "CMake"
17    }
18    fn extensions(&self) -> &'static [&'static str] {
19        &["cmake"]
20    }
21    fn grammar_name(&self) -> &'static str {
22        "cmake"
23    }
24
25    fn has_symbols(&self) -> bool {
26        true
27    }
28
29    fn container_kinds(&self) -> &'static [&'static str] {
30        &["function_def", "macro_def"]
31    }
32
33    fn function_kinds(&self) -> &'static [&'static str] {
34        &["function_def", "macro_def"]
35    }
36
37    fn type_kinds(&self) -> &'static [&'static str] {
38        &[]
39    }
40
41    fn import_kinds(&self) -> &'static [&'static str] {
42        &["normal_command"] // include(), find_package()
43    }
44
45    fn public_symbol_kinds(&self) -> &'static [&'static str] {
46        &["function_def", "macro_def"]
47    }
48
49    fn visibility_mechanism(&self) -> VisibilityMechanism {
50        VisibilityMechanism::NotApplicable
51    }
52
53    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
54        let name = match self.node_name(node, content) {
55            Some(n) => n.to_string(),
56            None => return Vec::new(),
57        };
58
59        let kind = match node.kind() {
60            "function_def" | "macro_def" => SymbolKind::Function,
61            _ => return Vec::new(),
62        };
63
64        vec![Export {
65            name,
66            kind,
67            line: node.start_position().row + 1,
68        }]
69    }
70
71    fn scope_creating_kinds(&self) -> &'static [&'static str] {
72        &["function_def", "macro_def"]
73    }
74
75    fn control_flow_kinds(&self) -> &'static [&'static str] {
76        &["if_condition", "foreach_loop", "while_loop"]
77    }
78
79    fn complexity_nodes(&self) -> &'static [&'static str] {
80        &[
81            "if_condition",
82            "elseif_command",
83            "foreach_loop",
84            "while_loop",
85        ]
86    }
87
88    fn nesting_nodes(&self) -> &'static [&'static str] {
89        &["function_def", "macro_def", "if_condition", "foreach_loop"]
90    }
91
92    fn signature_suffix(&self) -> &'static str {
93        ""
94    }
95
96    fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
97        let name = self.node_name(node, content)?;
98        Some(simple_function_symbol(
99            node,
100            content,
101            name,
102            self.extract_docstring(node, content),
103        ))
104    }
105
106    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
107        self.extract_function(node, content, false)
108    }
109
110    fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
111        None
112    }
113
114    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
115        // CMake uses # for comments
116        let mut prev = node.prev_sibling();
117        let mut doc_lines = Vec::new();
118
119        while let Some(sibling) = prev {
120            let text = &content[sibling.byte_range()];
121            if sibling.kind() == "line_comment" {
122                let line = text.strip_prefix('#').unwrap_or(text).trim();
123                doc_lines.push(line.to_string());
124                prev = sibling.prev_sibling();
125            } else {
126                break;
127            }
128        }
129
130        if doc_lines.is_empty() {
131            return None;
132        }
133
134        doc_lines.reverse();
135        Some(doc_lines.join(" "))
136    }
137
138    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
139        Vec::new()
140    }
141
142    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
143        if node.kind() != "normal_command" {
144            return Vec::new();
145        }
146
147        let text = &content[node.byte_range()];
148        let line = node.start_position().row + 1;
149
150        // include(file), find_package(pkg)
151        if text.starts_with("include(") || text.starts_with("find_package(") {
152            let inner = text
153                .split('(')
154                .nth(1)
155                .and_then(|s| s.split(')').next())
156                .map(|s| s.trim().to_string());
157
158            if let Some(module) = inner {
159                return vec![Import {
160                    module,
161                    names: Vec::new(),
162                    alias: None,
163                    is_wildcard: false,
164                    is_relative: text.starts_with("include("),
165                    line,
166                }];
167            }
168        }
169
170        Vec::new()
171    }
172
173    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
174        // CMake: include(file) or find_package(pkg)
175        format!("include({})", import.module)
176    }
177
178    fn is_public(&self, _node: &Node, _content: &str) -> bool {
179        true
180    }
181    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
182        Visibility::Public
183    }
184
185    fn is_test_symbol(&self, _symbol: &crate::Symbol) -> bool {
186        false
187    }
188
189    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
190        None
191    }
192
193    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
194        node.child_by_field_name("body")
195    }
196
197    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
198        false
199    }
200
201    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
202        // function(name args...) - name is first argument
203        let mut cursor = node.walk();
204        for child in node.children(&mut cursor) {
205            if child.kind() == "argument" {
206                return Some(&content[child.byte_range()]);
207            }
208        }
209        None
210    }
211
212    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
213        let name = path.file_name()?.to_str()?;
214        if name == "CMakeLists.txt" || name.ends_with(".cmake") {
215            let stem = path.file_stem()?.to_str()?;
216            return Some(stem.to_string());
217        }
218        None
219    }
220
221    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
222        vec![
223            format!("{}.cmake", module),
224            format!("cmake/{}.cmake", module),
225        ]
226    }
227
228    fn lang_key(&self) -> &'static str {
229        "cmake"
230    }
231
232    fn is_stdlib_import(&self, _import_name: &str, _project_root: &Path) -> bool {
233        false
234    }
235    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
236        None
237    }
238
239    fn resolve_local_import(
240        &self,
241        import: &str,
242        _current_file: &Path,
243        project_root: &Path,
244    ) -> Option<PathBuf> {
245        let candidates = [
246            project_root.join("cmake").join(format!("{}.cmake", import)),
247            project_root.join(format!("{}.cmake", import)),
248        ];
249        for c in &candidates {
250            if c.is_file() {
251                return Some(c.clone());
252            }
253        }
254        None
255    }
256
257    fn resolve_external_import(
258        &self,
259        _import_name: &str,
260        _project_root: &Path,
261    ) -> Option<ResolvedPackage> {
262        None
263    }
264
265    fn get_version(&self, project_root: &Path) -> Option<String> {
266        if project_root.join("CMakeLists.txt").is_file() {
267            return Some("cmake".to_string());
268        }
269        None
270    }
271
272    fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
273        None
274    }
275    fn indexable_extensions(&self) -> &'static [&'static str] {
276        &["cmake"]
277    }
278    fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
279        Vec::new()
280    }
281
282    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
283        use crate::traits::skip_dotfiles;
284        if skip_dotfiles(name) {
285            return true;
286        }
287        if is_dir && name == "build" {
288            return true;
289        }
290        !is_dir && !name.ends_with(".cmake") && name != "CMakeLists.txt"
291    }
292
293    fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
294        Vec::new()
295    }
296
297    fn package_module_name(&self, entry_name: &str) -> String {
298        entry_name
299            .strip_suffix(".cmake")
300            .unwrap_or(entry_name)
301            .to_string()
302    }
303
304    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
305        if path.is_file() {
306            return Some(path.to_path_buf());
307        }
308        let cmakelists = path.join("CMakeLists.txt");
309        if cmakelists.is_file() {
310            return Some(cmakelists);
311        }
312        None
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use crate::validate_unused_kinds_audit;
320
321    #[test]
322    fn unused_node_kinds_audit() {
323        #[rustfmt::skip]
324        let documented_unused: &[&str] = &[
325            "block", "block_command", "block_def", "body", "else", "else_command",
326            "elseif", "endblock", "endblock_command", "endforeach", "endforeach_command",
327            "endfunction", "endfunction_command", "endif", "endif_command", "endwhile",
328            "endwhile_command", "foreach", "foreach_command", "function",
329            "function_command", "identifier", "if", "if_command", "while",
330            "while_command",
331        ];
332        validate_unused_kinds_audit(&CMake, documented_unused)
333            .expect("CMake unused node kinds audit failed");
334    }
335}