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 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 has_symbols(&self) -> bool {
23 true
24 }
25
26 fn container_kinds(&self) -> &'static [&'static str] {
27 &["type_definition", "type_alias"]
28 }
29
30 fn function_kinds(&self) -> &'static [&'static str] {
31 &["function"]
32 }
33
34 fn type_kinds(&self) -> &'static [&'static str] {
35 &["type_definition", "type_alias"]
36 }
37
38 fn import_kinds(&self) -> &'static [&'static str] {
39 &["import"]
40 }
41
42 fn public_symbol_kinds(&self) -> &'static [&'static str] {
43 &["function", "type_definition", "type_alias", "constant"]
44 }
45
46 fn visibility_mechanism(&self) -> VisibilityMechanism {
47 VisibilityMechanism::ExplicitExport }
49
50 fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
51 let text = &content[node.byte_range()];
52
53 if !text.starts_with("pub ") {
55 return Vec::new();
56 }
57
58 let name = match self.node_name(node, content) {
59 Some(n) => n.to_string(),
60 None => return Vec::new(),
61 };
62
63 let kind = match node.kind() {
64 "function" => SymbolKind::Function,
65 "type_definition" => SymbolKind::Type,
66 "type_alias" => SymbolKind::Type,
67 "constant" => SymbolKind::Variable,
68 _ => return Vec::new(),
69 };
70
71 vec![Export {
72 name,
73 kind,
74 line: node.start_position().row + 1,
75 }]
76 }
77
78 fn scope_creating_kinds(&self) -> &'static [&'static str] {
79 &["function", "anonymous_function"]
80 }
81
82 fn control_flow_kinds(&self) -> &'static [&'static str] {
83 &["case", "if"]
84 }
85
86 fn complexity_nodes(&self) -> &'static [&'static str] {
87 &["case", "case_clause", "if"]
88 }
89
90 fn nesting_nodes(&self) -> &'static [&'static str] {
91 &["function", "case", "block"]
92 }
93
94 fn signature_suffix(&self) -> &'static str {
95 ""
96 }
97
98 fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
99 if node.kind() != "function" {
100 return None;
101 }
102
103 let name = self.node_name(node, content)?;
104 let text = &content[node.byte_range()];
105 let first_line = text.lines().next().unwrap_or(text);
106 let is_public = text.starts_with("pub ");
107
108 Some(Symbol {
109 name: name.to_string(),
110 kind: SymbolKind::Function,
111 signature: first_line.trim().to_string(),
112 docstring: self.extract_docstring(node, content),
113 attributes: Vec::new(),
114 start_line: node.start_position().row + 1,
115 end_line: node.end_position().row + 1,
116 visibility: if is_public {
117 Visibility::Public
118 } else {
119 Visibility::Private
120 },
121 children: Vec::new(),
122 is_interface_impl: false,
123 implements: Vec::new(),
124 })
125 }
126
127 fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
128 let name = self.node_name(node, content)?;
129 let text = &content[node.byte_range()];
130 let first_line = text.lines().next().unwrap_or(text);
131 let is_public = text.starts_with("pub ");
132
133 let kind = match node.kind() {
134 "type_definition" => SymbolKind::Type,
135 "type_alias" => SymbolKind::Type,
136 _ => return None,
137 };
138
139 Some(Symbol {
140 name: name.to_string(),
141 kind,
142 signature: first_line.trim().to_string(),
143 docstring: self.extract_docstring(node, content),
144 attributes: Vec::new(),
145 start_line: node.start_position().row + 1,
146 end_line: node.end_position().row + 1,
147 visibility: if is_public {
148 Visibility::Public
149 } else {
150 Visibility::Private
151 },
152 children: Vec::new(),
153 is_interface_impl: false,
154 implements: Vec::new(),
155 })
156 }
157
158 fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
159 self.extract_container(node, content)
160 }
161
162 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
163 let mut prev = node.prev_sibling();
165 let mut doc_lines = Vec::new();
166
167 while let Some(sibling) = prev {
168 let text = &content[sibling.byte_range()];
169 if sibling.kind() == "comment" && text.starts_with("///") {
170 let line = text.strip_prefix("///").unwrap_or(text).trim();
171 doc_lines.push(line.to_string());
172 prev = sibling.prev_sibling();
173 } else {
174 break;
175 }
176 }
177
178 if doc_lines.is_empty() {
179 return None;
180 }
181
182 doc_lines.reverse();
183 Some(doc_lines.join(" "))
184 }
185
186 fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
187 Vec::new()
188 }
189
190 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
191 if node.kind() != "import" {
192 return Vec::new();
193 }
194
195 let text = &content[node.byte_range()];
196 let line = node.start_position().row + 1;
197
198 if let Some(rest) = text.strip_prefix("import ") {
200 let module = rest.split_whitespace().next().unwrap_or("").to_string();
201
202 if !module.is_empty() {
203 return vec![Import {
204 module,
205 names: Vec::new(),
206 alias: None,
207 is_wildcard: false,
208 is_relative: false,
209 line,
210 }];
211 }
212 }
213
214 Vec::new()
215 }
216
217 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
218 let names_to_use: Vec<&str> = names
220 .map(|n| n.to_vec())
221 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
222 if names_to_use.is_empty() {
223 format!("import {}", import.module)
224 } else {
225 format!("import {}.{{{}}}", import.module, names_to_use.join(", "))
226 }
227 }
228
229 fn is_public(&self, node: &Node, content: &str) -> bool {
230 content[node.byte_range()].starts_with("pub ")
231 }
232
233 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
234 if self.is_public(node, content) {
235 Visibility::Public
236 } else {
237 Visibility::Private
238 }
239 }
240
241 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
242 let name = symbol.name.as_str();
243 match symbol.kind {
244 crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
245 crate::SymbolKind::Module => name == "tests" || name == "test",
246 _ => false,
247 }
248 }
249
250 fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
251 None
252 }
253
254 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
255 node.child_by_field_name("body")
256 }
257
258 fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
259 false
260 }
261
262 fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
263 node.child_by_field_name("name")
264 .map(|n| &content[n.byte_range()])
265 }
266
267 fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
268 let ext = path.extension()?.to_str()?;
269 if ext != "gleam" {
270 return None;
271 }
272 let stem = path.file_stem()?.to_str()?;
273 Some(stem.to_string())
274 }
275
276 fn module_name_to_paths(&self, module: &str) -> Vec<String> {
277 let path = module.replace('/', "/");
278 vec![format!("{}.gleam", path), format!("src/{}.gleam", path)]
279 }
280
281 fn lang_key(&self) -> &'static str {
282 "gleam"
283 }
284
285 fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
286 import_name.starts_with("gleam/")
287 }
288
289 fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
290 None
291 }
292
293 fn resolve_local_import(
294 &self,
295 import: &str,
296 _current_file: &Path,
297 project_root: &Path,
298 ) -> Option<PathBuf> {
299 let path = import.replace('/', "/");
300 let full = project_root.join("src").join(format!("{}.gleam", path));
301 if full.is_file() { Some(full) } else { None }
302 }
303
304 fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
305 None
306 }
307
308 fn get_version(&self, project_root: &Path) -> Option<String> {
309 if project_root.join("gleam.toml").is_file() {
310 return Some("gleam.toml".to_string());
311 }
312 None
313 }
314
315 fn find_package_cache(&self, project_root: &Path) -> Option<PathBuf> {
316 let build = project_root.join("build/packages");
317 if build.is_dir() {
318 return Some(build);
319 }
320 None
321 }
322
323 fn indexable_extensions(&self) -> &'static [&'static str] {
324 &["gleam"]
325 }
326 fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
327 Vec::new()
328 }
329
330 fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
331 use crate::traits::{has_extension, skip_dotfiles};
332 if skip_dotfiles(name) {
333 return true;
334 }
335 if is_dir && name == "build" {
336 return true;
337 }
338 !is_dir && !has_extension(name, self.indexable_extensions())
339 }
340
341 fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
342 Vec::new()
343 }
344
345 fn package_module_name(&self, entry_name: &str) -> String {
346 entry_name
347 .strip_suffix(".gleam")
348 .unwrap_or(entry_name)
349 .to_string()
350 }
351
352 fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
353 if path.is_file() {
354 Some(path.to_path_buf())
355 } else {
356 None
357 }
358 }
359}
360
361#[cfg(test)]
362mod tests {
363 use super::*;
364 use crate::validate_unused_kinds_audit;
365
366 #[test]
367 fn unused_node_kinds_audit() {
368 #[rustfmt::skip]
369 let documented_unused: &[&str] = &[
370 "data_constructor", "data_constructor_argument", "data_constructor_arguments",
372 "data_constructors", "external_type", "function_parameter", "function_parameter_types",
373 "function_parameters", "function_type", "opacity_modifier", "remote_type_identifier",
374 "tuple_type", "type", "type_argument", "type_arguments", "type_hole", "type_identifier",
375 "type_name", "type_parameter", "type_parameters", "type_var", "visibility_modifier",
376 "case_clause_guard", "case_clause_pattern", "case_clause_patterns", "case_clauses",
378 "case_subjects",
379 "binary_expression", "constructor_name", "external_function", "external_function_body",
381 "function_call", "remote_constructor_name",
382 "unqualified_import", "unqualified_imports",
384 "identifier", "module", "module_comment", "statement_comment",
386 ];
387 validate_unused_kinds_audit(&Gleam, documented_unused)
388 .expect("Gleam unused node kinds audit failed");
389 }
390}