Skip to main content

normalize_languages/
gleam.rs

1//! Gleam language support.
2
3use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6/// Gleam language support.
7pub struct Gleam;
8
9impl Language for Gleam {
10    fn name(&self) -> &'static str {
11        "Gleam"
12    }
13    fn extensions(&self) -> &'static [&'static str] {
14        &["gleam"]
15    }
16    fn grammar_name(&self) -> &'static str {
17        "gleam"
18    }
19
20    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
21        Some(self)
22    }
23
24    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
25        if node.kind() != "import" {
26            return Vec::new();
27        }
28
29        let text = &content[node.byte_range()];
30        let line = node.start_position().row + 1;
31
32        // import module/path
33        if let Some(rest) = text.strip_prefix("import ") {
34            let module = rest.split_whitespace().next().unwrap_or("").to_string();
35
36            if !module.is_empty() {
37                return vec![Import {
38                    module,
39                    names: Vec::new(),
40                    alias: None,
41                    is_wildcard: false,
42                    is_relative: false,
43                    line,
44                }];
45            }
46        }
47
48        Vec::new()
49    }
50
51    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
52        // Gleam: import module or import module.{a, b, c}
53        let names_to_use: Vec<&str> = names
54            .map(|n| n.to_vec())
55            .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
56        if names_to_use.is_empty() {
57            format!("import {}", import.module)
58        } else {
59            format!("import {}.{{{}}}", import.module, names_to_use.join(", "))
60        }
61    }
62
63    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
64        let mut doc_lines: Vec<String> = Vec::new();
65        let mut prev = node.prev_sibling();
66
67        while let Some(sibling) = prev {
68            let kind = sibling.kind();
69            if kind == "comment" || kind == "statement_comment" {
70                let text = &content[sibling.byte_range()];
71                // Doc comments start with ///
72                if let Some(line) = text.strip_prefix("///") {
73                    let line = line.strip_prefix(' ').unwrap_or(line);
74                    doc_lines.push(line.to_string());
75                } else {
76                    break;
77                }
78            } else {
79                break;
80            }
81            prev = sibling.prev_sibling();
82        }
83
84        if doc_lines.is_empty() {
85            return None;
86        }
87
88        doc_lines.reverse();
89        let joined = doc_lines.join("\n").trim().to_string();
90        if joined.is_empty() {
91            None
92        } else {
93            Some(joined)
94        }
95    }
96
97    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
98        if content[node.byte_range()].starts_with("pub ") {
99            Visibility::Public
100        } else {
101            Visibility::Private
102        }
103    }
104
105    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
106        let name = symbol.name.as_str();
107        match symbol.kind {
108            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
109            crate::SymbolKind::Module => name == "tests" || name == "test",
110            _ => false,
111        }
112    }
113
114    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
115        node.child_by_field_name("body")
116    }
117
118    fn analyze_container_body(
119        &self,
120        body_node: &Node,
121        content: &str,
122        inner_indent: &str,
123    ) -> Option<ContainerBody> {
124        crate::body::analyze_brace_body(body_node, content, inner_indent)
125    }
126}
127
128impl LanguageSymbols for Gleam {}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::validate_unused_kinds_audit;
134
135    #[test]
136    fn unused_node_kinds_audit() {
137        #[rustfmt::skip]
138        let documented_unused: &[&str] = &[
139            // Type-related nodes
140            "data_constructor", "data_constructor_argument", "data_constructor_arguments",
141            "data_constructors", "external_type", "function_parameter", "function_parameter_types",
142            "function_parameters", "function_type", "opacity_modifier", "remote_type_identifier",
143            "tuple_type", "type", "type_argument", "type_arguments", "type_hole", "type_identifier",
144            "type_name", "type_parameter", "type_parameters", "type_var", "visibility_modifier",
145            // Case clause patterns
146            "case_clause_guard", "case_clause_pattern", "case_clause_patterns", "case_clauses",
147            "case_subjects",
148            // Function-related nodes
149            "binary_expression", "constructor_name", "external_function", "external_function_body",
150            "function_call", "remote_constructor_name",
151            // Import-related nodes
152            "unqualified_import", "unqualified_imports",
153            // Comments and identifiers
154            "identifier", "module", "module_comment", "statement_comment",
155            // structural node, not extracted as symbols
156            "block",
157            "import",
158            "anonymous_function",
159            "case_clause",
160            "case",
161        ];
162        validate_unused_kinds_audit(&Gleam, documented_unused)
163            .expect("Gleam unused node kinds audit failed");
164    }
165}