Skip to main content

normalize_languages/
ini.rs

1//! INI configuration file 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/// INI language support.
9pub struct Ini;
10
11impl Language for Ini {
12    fn name(&self) -> &'static str {
13        "INI"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["ini", "cfg", "conf", "properties"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "ini"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &["section"]
28    }
29
30    fn function_kinds(&self) -> &'static [&'static str] {
31        &[]
32    }
33    fn type_kinds(&self) -> &'static [&'static str] {
34        &[]
35    }
36    fn import_kinds(&self) -> &'static [&'static str] {
37        &[]
38    }
39
40    fn public_symbol_kinds(&self) -> &'static [&'static str] {
41        &["section", "setting"]
42    }
43
44    fn visibility_mechanism(&self) -> VisibilityMechanism {
45        VisibilityMechanism::AllPublic
46    }
47
48    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
49        match node.kind() {
50            "section" => {
51                if let Some(name) = self.node_name(node, content) {
52                    return vec![Export {
53                        name: name.to_string(),
54                        kind: SymbolKind::Module,
55                        line: node.start_position().row + 1,
56                    }];
57                }
58            }
59            "setting" => {
60                if let Some(name) = self.node_name(node, content) {
61                    return vec![Export {
62                        name: name.to_string(),
63                        kind: SymbolKind::Variable,
64                        line: node.start_position().row + 1,
65                    }];
66                }
67            }
68            _ => {}
69        }
70        Vec::new()
71    }
72
73    fn scope_creating_kinds(&self) -> &'static [&'static str] {
74        &["section"]
75    }
76
77    fn control_flow_kinds(&self) -> &'static [&'static str] {
78        &[]
79    }
80    fn complexity_nodes(&self) -> &'static [&'static str] {
81        &[]
82    }
83    fn nesting_nodes(&self) -> &'static [&'static str] {
84        &["section"]
85    }
86
87    fn signature_suffix(&self) -> &'static str {
88        ""
89    }
90
91    fn extract_function(
92        &self,
93        _node: &Node,
94        _content: &str,
95        _in_container: bool,
96    ) -> Option<Symbol> {
97        None
98    }
99
100    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
101        if node.kind() != "section" {
102            return None;
103        }
104
105        let name = self.node_name(node, content)?;
106
107        Some(Symbol {
108            name: name.to_string(),
109            kind: SymbolKind::Module,
110            signature: format!("[{}]", name),
111            docstring: None,
112            attributes: Vec::new(),
113            start_line: node.start_position().row + 1,
114            end_line: node.end_position().row + 1,
115            visibility: Visibility::Public,
116            children: Vec::new(),
117            is_interface_impl: false,
118            implements: Vec::new(),
119        })
120    }
121
122    fn extract_type(&self, _node: &Node, _content: &str) -> Option<Symbol> {
123        None
124    }
125    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
126        None
127    }
128
129    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
130        Vec::new()
131    }
132    fn extract_imports(&self, _node: &Node, _content: &str) -> Vec<Import> {
133        Vec::new()
134    }
135
136    fn format_import(&self, _import: &Import, _names: Option<&[&str]>) -> String {
137        // INI has no imports
138        String::new()
139    }
140
141    fn is_public(&self, _node: &Node, _content: &str) -> bool {
142        true
143    }
144    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
145        Visibility::Public
146    }
147
148    fn is_test_symbol(&self, _symbol: &crate::Symbol) -> bool {
149        false
150    }
151
152    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
153        None
154    }
155
156    fn container_body<'a>(&self, _node: &'a Node<'a>) -> Option<Node<'a>> {
157        None
158    }
159    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
160        false
161    }
162
163    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
164        node.child_by_field_name("name")
165            .map(|n| &content[n.byte_range()])
166            .map(|s| s.trim_matches(|c| c == '[' || c == ']'))
167    }
168
169    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
170        let ext = path.extension()?.to_str()?;
171        if !["ini", "cfg", "conf", "properties"].contains(&ext) {
172            return None;
173        }
174        let stem = path.file_stem()?.to_str()?;
175        Some(stem.to_string())
176    }
177
178    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
179        vec![
180            format!("{}.ini", module),
181            format!("{}.cfg", module),
182            format!("{}.conf", module),
183        ]
184    }
185
186    fn lang_key(&self) -> &'static str {
187        "ini"
188    }
189
190    fn is_stdlib_import(&self, _: &str, _: &Path) -> bool {
191        false
192    }
193    fn find_stdlib(&self, _: &Path) -> Option<PathBuf> {
194        None
195    }
196    fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
197        None
198    }
199    fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
200        None
201    }
202    fn get_version(&self, _: &Path) -> Option<String> {
203        None
204    }
205    fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
206        None
207    }
208    fn indexable_extensions(&self) -> &'static [&'static str] {
209        &["ini", "cfg", "conf"]
210    }
211    fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
212        Vec::new()
213    }
214
215    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
216        use crate::traits::{has_extension, skip_dotfiles};
217        if skip_dotfiles(name) {
218            return true;
219        }
220        !is_dir && !has_extension(name, self.indexable_extensions())
221    }
222
223    fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
224        Vec::new()
225    }
226
227    fn package_module_name(&self, entry_name: &str) -> String {
228        entry_name
229            .strip_suffix(".ini")
230            .or_else(|| entry_name.strip_suffix(".cfg"))
231            .or_else(|| entry_name.strip_suffix(".conf"))
232            .unwrap_or(entry_name)
233            .to_string()
234    }
235
236    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
237        if path.is_file() {
238            Some(path.to_path_buf())
239        } else {
240            None
241        }
242    }
243}
244
245#[cfg(test)]
246mod tests {
247    use super::*;
248    use crate::validate_unused_kinds_audit;
249
250    #[test]
251    fn unused_node_kinds_audit() {
252        #[rustfmt::skip]
253        let documented_unused: &[&str] = &[];
254        validate_unused_kinds_audit(&Ini, documented_unused)
255            .expect("INI unused node kinds audit failed");
256    }
257}