1use 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
11pub 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"] }
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 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 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 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 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}