1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8pub struct Nix;
10
11impl Language for Nix {
12 fn name(&self) -> &'static str {
13 "Nix"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["nix"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "nix"
20 }
21
22 fn has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &[
28 "attrset_expression",
29 "let_expression",
30 "rec_attrset_expression",
31 ]
32 }
33
34 fn function_kinds(&self) -> &'static [&'static str] {
35 &["function_expression"]
36 }
37
38 fn type_kinds(&self) -> &'static [&'static str] {
39 &[]
40 }
41
42 fn import_kinds(&self) -> &'static [&'static str] {
43 &["apply_expression"] }
45
46 fn public_symbol_kinds(&self) -> &'static [&'static str] {
47 &["binding"]
48 }
49
50 fn visibility_mechanism(&self) -> VisibilityMechanism {
51 VisibilityMechanism::NotApplicable
52 }
53
54 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
55 if node.kind() != "binding" {
56 return Vec::new();
57 }
58
59 let name = match self.node_name(node, content) {
60 Some(n) => n.to_string(),
61 None => return Vec::new(),
62 };
63
64 vec![Export {
65 name,
66 kind: SymbolKind::Variable,
67 line: node.start_position().row + 1,
68 }]
69 }
70
71 fn scope_creating_kinds(&self) -> &'static [&'static str] {
72 &["let_expression", "with_expression", "function_expression"]
73 }
74
75 fn control_flow_kinds(&self) -> &'static [&'static str] {
76 &["if_expression"]
77 }
78
79 fn complexity_nodes(&self) -> &'static [&'static str] {
80 &["if_expression"]
81 }
82
83 fn nesting_nodes(&self) -> &'static [&'static str] {
84 &[
85 "attrset_expression",
86 "let_expression",
87 "function_expression",
88 ]
89 }
90
91 fn signature_suffix(&self) -> &'static str {
92 ""
93 }
94
95 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
96 if node.kind() != "function_expression" {
97 return None;
98 }
99
100 let text = &content[node.byte_range()];
101 let first_line = text.lines().next().unwrap_or(text);
102
103 let name = node
105 .parent()
106 .filter(|p| p.kind() == "binding")
107 .and_then(|p| p.child_by_field_name("attrpath"))
108 .map(|n| content[n.byte_range()].to_string())
109 .unwrap_or_else(|| "<lambda>".to_string());
110
111 Some(Symbol {
112 name,
113 kind: SymbolKind::Function,
114 signature: first_line.trim().chars().take(80).collect(),
115 docstring: self.extract_docstring(node, content),
116 attributes: Vec::new(),
117 start_line: node.start_position().row + 1,
118 end_line: node.end_position().row + 1,
119 visibility: Visibility::Public,
120 children: Vec::new(),
121 is_interface_impl: false,
122 implements: Vec::new(),
123 })
124 }
125
126 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
127 let kind_str = node.kind();
128 if !matches!(
129 kind_str,
130 "attrset_expression" | "let_expression" | "rec_attrset_expression"
131 ) {
132 return None;
133 }
134
135 let name = node
137 .parent()
138 .filter(|p| p.kind() == "binding")
139 .and_then(|p| p.child_by_field_name("attrpath"))
140 .map(|n| content[n.byte_range()].to_string())
141 .unwrap_or_else(|| match kind_str {
142 "let_expression" => "let".to_string(),
143 "rec_attrset_expression" => "rec { }".to_string(),
144 _ => "{ }".to_string(),
145 });
146
147 Some(Symbol {
148 name: name.clone(),
149 kind: SymbolKind::Module,
150 signature: name,
151 docstring: None,
152 attributes: Vec::new(),
153 start_line: node.start_position().row + 1,
154 end_line: node.end_position().row + 1,
155 visibility: Visibility::Public,
156 children: Vec::new(),
157 is_interface_impl: false,
158 implements: Vec::new(),
159 })
160 }
161
162 fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
163 None
164 }
165
166 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
167 let mut prev = node.prev_sibling();
169 let mut doc_lines = Vec::new();
170
171 while let Some(sibling) = prev {
172 let text = &content[sibling.byte_range()];
173 if sibling.kind() == "comment" && text.starts_with('#') {
174 let line = text.strip_prefix('#').unwrap_or(text).trim();
175 doc_lines.push(line.to_string());
176 prev = sibling.prev_sibling();
177 } else {
178 break;
179 }
180 }
181
182 if doc_lines.is_empty() {
183 return None;
184 }
185
186 doc_lines.reverse();
187 Some(doc_lines.join(" "))
188 }
189
190 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
191 Vec::new()
192 }
193
194 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
195 if node.kind() != "apply_expression" {
196 return Vec::new();
197 }
198
199 let text = &content[node.byte_range()];
200 if !text.starts_with("import ") {
201 return Vec::new();
202 }
203
204 let rest = text.strip_prefix("import ").unwrap_or("").trim();
206 let module = rest.split_whitespace().next().unwrap_or(rest).to_string();
207
208 vec![Import {
209 module,
210 names: Vec::new(),
211 alias: None,
212 is_wildcard: false,
213 is_relative: rest.starts_with('.'),
214 line: node.start_position().row + 1,
215 }]
216 }
217
218 fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
219 format!("import {}", import.module)
221 }
222
223 fn is_public(&self, _node: &Node, _content: &str) -> bool {
224 true
225 }
226 fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
227 Visibility::Public
228 }
229
230 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
231 let name = symbol.name.as_str();
232 match symbol.kind {
233 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
234 crate::SymbolKind::Module => name == "tests" || name == "test",
235 _ => false,
236 }
237 }
238
239 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
240 None
241 }
242
243 fn container_body<'a>(&self, _node: &'a Node<'a>) -> Option<Node<'a>> {
244 None
245 }
246 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
247 false
248 }
249
250 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
251 node.child_by_field_name("attrpath")
252 .map(|n| &content[n.byte_range()])
253 }
254
255 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
256 let ext = path.extension()?.to_str()?;
257 if ext != "nix" {
258 return None;
259 }
260 let stem = path.file_stem()?.to_str()?;
261 Some(stem.to_string())
262 }
263
264 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
265 vec![format!("{}.nix", module), format!("{}/default.nix", module)]
266 }
267
268 fn lang_key(&self) -> &'static str {
269 "nix"
270 }
271
272 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
273 import_name.starts_with("<nixpkgs") || import_name.starts_with("<nixos")
274 }
275
276 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
277 None
278 }
279
280 fn resolve_local_import(
281 &self,
282 import: &str,
283 current_file: &Path,
284 _project_root: &Path,
285 ) -> Option<PathBuf> {
286 if import.starts_with('.') {
287 let dir = current_file.parent()?;
288 let full = dir.join(import);
289 if full.is_file() {
290 return Some(full);
291 }
292 let default = full.join("default.nix");
293 if default.is_file() {
294 return Some(default);
295 }
296 }
297 None
298 }
299
300 fn resolve_external_import(
301 &self,
302 _import_name: &str,
303 _project_root: &Path,
304 ) -> Option<ResolvedPackage> {
305 None
306 }
307
308 fn get_version(&self, project_root: &Path) -> Option<String> {
309 if project_root.join("flake.nix").is_file() {
310 return Some("flake".to_string());
311 }
312 if project_root.join("default.nix").is_file() {
313 return Some("nix".to_string());
314 }
315 None
316 }
317
318 fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
319 PathBuf::from("/nix/store")
320 .is_dir()
321 .then(|| PathBuf::from("/nix/store"))
322 }
323
324 fn indexable_extensions(&self) -> &'static [&'static str] {
325 &["nix"]
326 }
327 fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
328 Vec::new()
329 }
330
331 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
332 use crate::traits::{has_extension, skip_dotfiles};
333 if skip_dotfiles(name) {
334 return true;
335 }
336 if is_dir && name == "result" {
337 return true;
338 }
339 !is_dir && !has_extension(name, self.indexable_extensions())
340 }
341
342 fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
343 Vec::new()
344 }
345
346 fn package_module_name(&self, entry_name: &str) -> String {
347 entry_name
348 .strip_suffix(".nix")
349 .unwrap_or(entry_name)
350 .to_string()
351 }
352
353 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
354 if path.is_file() {
355 return Some(path.to_path_buf());
356 }
357 let default = path.join("default.nix");
358 if default.is_file() {
359 return Some(default);
360 }
361 None
362 }
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use crate::validate_unused_kinds_audit;
369
370 #[test]
371 fn unused_node_kinds_audit() {
372 #[rustfmt::skip]
373 let documented_unused: &[&str] = &[
374 "assert_expression", "binary_expression", "float_expression",
375 "formal", "formals", "has_attr_expression", "hpath_expression",
376 "identifier", "indented_string_expression", "integer_expression",
377 "list_expression", "let_attrset_expression", "parenthesized_expression",
378 "path_expression", "select_expression", "spath_expression",
379 "string_expression", "unary_expression", "uri_expression",
380 "variable_expression",
381 ];
382 validate_unused_kinds_audit(&Nix, documented_unused)
383 .expect("Nix unused node kinds audit failed");
384 }
385}