1use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8pub struct Gleam;
10
11impl Language for Gleam {
12 fn name(&self) -> &'static str {
13 "Gleam"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["gleam"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "gleam"
20 }
21
22 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
23 Some(self)
24 }
25
26 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
27 if node.kind() != "import" {
28 return Vec::new();
29 }
30
31 let text = &content[node.byte_range()];
32 let line = node.start_position().row + 1;
33
34 if let Some(rest) = text.strip_prefix("import ") {
36 let module = rest.split_whitespace().next().unwrap_or("").to_string();
37
38 if !module.is_empty() {
39 return vec![Import {
40 module,
41 names: Vec::new(),
42 alias: None,
43 is_wildcard: false,
44 is_relative: false,
45 line,
46 }];
47 }
48 }
49
50 Vec::new()
51 }
52
53 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
54 let names_to_use: Vec<&str> = names
56 .map(|n| n.to_vec())
57 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
58 if names_to_use.is_empty() {
59 format!("import {}", import.module)
60 } else {
61 format!("import {}.{{{}}}", import.module, names_to_use.join(", "))
62 }
63 }
64
65 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
66 let mut doc_lines: Vec<String> = Vec::new();
67 let mut prev = node.prev_sibling();
68
69 while let Some(sibling) = prev {
70 let kind = sibling.kind();
71 if kind == "comment" || kind == "statement_comment" {
72 let text = &content[sibling.byte_range()];
73 if let Some(line) = text.strip_prefix("///") {
75 let line = line.strip_prefix(' ').unwrap_or(line);
76 doc_lines.push(line.to_string());
77 } else {
78 break;
79 }
80 } else {
81 break;
82 }
83 prev = sibling.prev_sibling();
84 }
85
86 if doc_lines.is_empty() {
87 return None;
88 }
89
90 doc_lines.reverse();
91 let joined = doc_lines.join("\n").trim().to_string();
92 if joined.is_empty() {
93 None
94 } else {
95 Some(joined)
96 }
97 }
98
99 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
100 if content[node.byte_range()].starts_with("pub ") {
101 Visibility::Public
102 } else {
103 Visibility::Private
104 }
105 }
106
107 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
108 let name = symbol.name.as_str();
109 match symbol.kind {
110 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
111 crate::SymbolKind::Module => name == "tests" || name == "test",
112 _ => false,
113 }
114 }
115
116 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
117 node.child_by_field_name("body")
118 }
119
120 fn analyze_container_body(
121 &self,
122 body_node: &Node,
123 content: &str,
124 inner_indent: &str,
125 ) -> Option<ContainerBody> {
126 crate::body::analyze_brace_body(body_node, content, inner_indent)
127 }
128
129 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
130 static RESOLVER: GleamModuleResolver = GleamModuleResolver;
131 Some(&RESOLVER)
132 }
133}
134
135impl LanguageSymbols for Gleam {}
136
137pub struct GleamModuleResolver;
147
148impl ModuleResolver for GleamModuleResolver {
149 fn workspace_config(&self, root: &Path) -> ResolverConfig {
150 let mut path_mappings: Vec<(String, PathBuf)> = Vec::new();
151
152 let gleam_toml = root.join("gleam.toml");
153 if let Ok(content) = std::fs::read_to_string(&gleam_toml) {
154 for line in content.lines() {
155 let trimmed = line.trim();
156 if let Some(rest) = trimmed.strip_prefix("name") {
157 let rest = rest.trim_start_matches([' ', '=']).trim();
158 let name = rest.trim_matches('"').trim_matches('\'');
159 if !name.is_empty() {
160 path_mappings.push((name.to_string(), root.join("src")));
161 break;
162 }
163 }
164 }
165 }
166
167 ResolverConfig {
168 workspace_root: root.to_path_buf(),
169 path_mappings,
170 search_roots: vec![root.join("src"), root.join("test")],
171 }
172 }
173
174 fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
175 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
176 if ext != "gleam" {
177 return Vec::new();
178 }
179 for search_root in &cfg.search_roots {
180 if let Ok(rel) = file.strip_prefix(search_root) {
181 let module = rel
182 .to_str()
183 .unwrap_or("")
184 .trim_end_matches(".gleam")
185 .replace('\\', "/");
186 if !module.is_empty() {
187 if let Some((pkg, _)) = cfg.path_mappings.first() {
189 return vec![ModuleId {
190 canonical_path: format!("{}/{}", pkg, module),
191 }];
192 }
193 return vec![ModuleId {
194 canonical_path: module,
195 }];
196 }
197 }
198 }
199 Vec::new()
200 }
201
202 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
203 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
204 if ext != "gleam" {
205 return Resolution::NotApplicable;
206 }
207 let raw = &spec.raw;
208 let slash = raw.find('/');
211 let (pkg, path_in_pkg) = if let Some(idx) = slash {
212 (&raw[..idx], &raw[idx + 1..])
213 } else {
214 (raw.as_str(), "")
215 };
216
217 let exported_name = raw.rsplit('/').next().unwrap_or(raw).to_string();
218
219 for (own_pkg, src_dir) in &cfg.path_mappings {
221 if pkg == own_pkg {
222 let file_path = if path_in_pkg.is_empty() {
223 format!("{}.gleam", pkg)
224 } else {
225 format!("{}.gleam", path_in_pkg)
226 };
227 let candidate = src_dir.join(&file_path);
228 if candidate.exists() {
229 return Resolution::Resolved(candidate, exported_name);
230 }
231 return Resolution::NotFound;
232 }
233 }
234
235 Resolution::NotFound
237 }
238}
239
240#[cfg(test)]
241mod tests {
242 use super::*;
243 use crate::validate_unused_kinds_audit;
244
245 #[test]
246 fn unused_node_kinds_audit() {
247 #[rustfmt::skip]
248 let documented_unused: &[&str] = &[
249 "data_constructor", "data_constructor_argument", "data_constructor_arguments",
251 "data_constructors", "external_type", "function_parameter", "function_parameter_types",
252 "function_parameters", "function_type", "opacity_modifier", "remote_type_identifier",
253 "tuple_type", "type", "type_argument", "type_arguments", "type_hole", "type_identifier",
254 "type_name", "type_parameter", "type_parameters", "type_var", "visibility_modifier",
255 "case_clause_guard", "case_clause_pattern", "case_clause_patterns", "case_clauses",
257 "case_subjects",
258 "binary_expression", "constructor_name", "external_function", "external_function_body",
260 "function_call", "remote_constructor_name",
261 "unqualified_import", "unqualified_imports",
263 "identifier", "module", "module_comment", "statement_comment",
265 "block",
267 "import",
268 "anonymous_function",
269 "case_clause",
270 "case",
271 ];
272 validate_unused_kinds_audit(&Gleam, documented_unused)
273 .expect("Gleam unused node kinds audit failed");
274 }
275}