1use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use std::path::Path;
6use tree_sitter::Node;
7
8pub struct Scala;
10
11impl Language for Scala {
12 fn name(&self) -> &'static str {
13 "Scala"
14 }
15 fn extensions(&self) -> &'static [&'static str] {
16 &["scala", "sc"]
17 }
18 fn grammar_name(&self) -> &'static str {
19 "scala"
20 }
21
22 fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
23 Some(self)
24 }
25
26 fn signature_suffix(&self) -> &'static str {
27 " {}"
28 }
29
30 fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
31 extract_scaladoc(node, content)
32 }
33
34 fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
35 extract_scala_annotations(node, content)
36 }
37
38 fn refine_kind(
39 &self,
40 node: &Node,
41 _content: &str,
42 tag_kind: crate::SymbolKind,
43 ) -> crate::SymbolKind {
44 match node.kind() {
45 "trait_definition" => crate::SymbolKind::Trait,
46 _ => tag_kind,
47 }
48 }
49
50 fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
51 let mut implements = Vec::new();
52 let mut cursor = node.walk();
53 for child in node.children(&mut cursor) {
54 if child.kind() == "extends_clause" {
55 let mut ec = child.walk();
56 for t in child.children(&mut ec) {
57 if t.kind() == "type_identifier" {
58 implements.push(content[t.byte_range()].to_string());
59 }
60 }
61 }
62 }
63 crate::ImplementsInfo {
64 is_interface: false,
65 implements,
66 }
67 }
68
69 fn build_signature(&self, node: &Node, content: &str) -> String {
70 let name = match self.node_name(node, content) {
71 Some(n) => n,
72 None => {
73 return content[node.byte_range()]
74 .lines()
75 .next()
76 .unwrap_or("")
77 .trim()
78 .to_string();
79 }
80 };
81 match node.kind() {
82 "function_definition" | "function_declaration" => {
83 let params = node
84 .child_by_field_name("parameters")
85 .map(|p| content[p.byte_range()].to_string())
86 .unwrap_or_else(|| "()".to_string());
87 let ret = node
88 .child_by_field_name("return_type")
89 .map(|r| format!(": {}", &content[r.byte_range()]))
90 .unwrap_or_default();
91 format!("def {}{}{}", name, params, ret)
92 }
93 "class_definition" => format!("class {}", name),
94 "object_definition" => format!("object {}", name),
95 "trait_definition" => format!("trait {}", name),
96 _ => {
97 let text = &content[node.byte_range()];
98 text.lines().next().unwrap_or(text).trim().to_string()
99 }
100 }
101 }
102
103 fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
104 if node.kind() != "import_declaration" {
105 return Vec::new();
106 }
107
108 let text = &content[node.byte_range()];
109 let line = node.start_position().row + 1;
110
111 if let Some(rest) = text.strip_prefix("import ") {
113 let rest = rest.trim();
114 let is_wildcard = rest.ends_with("._") || rest.ends_with(".*");
115 let has_selectors = rest.contains('{');
116
117 if has_selectors {
118 if let Some(brace) = rest.find('{') {
120 let module = rest[..brace].trim_end_matches('.').to_string();
121 let inner = &rest[brace + 1..];
122 let inner = inner.strip_suffix('}').unwrap_or(inner);
123 let names: Vec<String> = inner
124 .split(',')
125 .map(|s| s.trim().to_string())
126 .filter(|s| !s.is_empty() && s != "_")
127 .collect();
128 return vec![Import {
129 module,
130 names,
131 alias: None,
132 is_wildcard: inner.contains('_'),
133 is_relative: false,
134 line,
135 }];
136 }
137 }
138
139 let module = if is_wildcard {
140 rest.strip_suffix("._")
141 .or_else(|| rest.strip_suffix(".*"))
142 .unwrap_or(rest)
143 .to_string()
144 } else {
145 rest.to_string()
146 };
147
148 return vec![Import {
149 module,
150 names: Vec::new(),
151 alias: None,
152 is_wildcard,
153 is_relative: false,
154 line,
155 }];
156 }
157
158 Vec::new()
159 }
160
161 fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
162 let names_to_use: Vec<&str> = names
164 .map(|n| n.to_vec())
165 .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
166 if import.is_wildcard {
167 format!("import {}._", import.module)
168 } else if names_to_use.is_empty() {
169 format!("import {}", import.module)
170 } else if names_to_use.len() == 1 {
171 format!("import {}.{}", import.module, names_to_use[0])
172 } else {
173 format!("import {}.{{{}}}", import.module, names_to_use.join(", "))
174 }
175 }
176
177 fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
178 {
179 let has_test_attr = symbol.attributes.iter().any(|a| a.contains("@Test"));
180 if has_test_attr {
181 return true;
182 }
183 match symbol.kind {
184 crate::SymbolKind::Class => {
185 symbol.name.starts_with("Test") || symbol.name.ends_with("Test")
186 }
187 _ => false,
188 }
189 }
190 }
191
192 fn test_file_globs(&self) -> &'static [&'static str] {
193 &[
194 "**/src/test/**/*.scala",
195 "**/*Test.scala",
196 "**/*Spec.scala",
197 "**/*Suite.scala",
198 ]
199 }
200
201 fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
202 let mut cursor = node.walk();
205 for child in node.children(&mut cursor) {
206 if child.kind() == "access_modifier" {
207 let text = &content[child.byte_range()];
208 if text.starts_with("private") {
209 return Visibility::Private;
210 }
211 if text.starts_with("protected") {
212 return Visibility::Protected;
213 }
214 }
215 if child.kind() == "modifiers" {
216 let mut mc = child.walk();
217 for m in child.children(&mut mc) {
218 if m.kind() == "access_modifier" {
219 let text = &content[m.byte_range()];
220 if text.starts_with("private") {
221 return Visibility::Private;
222 }
223 if text.starts_with("protected") {
224 return Visibility::Protected;
225 }
226 }
227 }
228 }
229 }
230 Visibility::Public
231 }
232
233 fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
234 node.child_by_field_name("body")
235 }
236 fn analyze_container_body(
237 &self,
238 body_node: &Node,
239 content: &str,
240 inner_indent: &str,
241 ) -> Option<ContainerBody> {
242 crate::body::analyze_brace_body(body_node, content, inner_indent)
243 }
244
245 fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
246 static RESOLVER: ScalaModuleResolver = ScalaModuleResolver;
247 Some(&RESOLVER)
248 }
249}
250
251impl LanguageSymbols for Scala {}
252
253pub struct ScalaModuleResolver;
259
260const SCALA_SRC_DIRS: &[&str] = &["src/main/scala", "src/test/scala", ""];
261
262impl ModuleResolver for ScalaModuleResolver {
263 fn workspace_config(&self, root: &Path) -> ResolverConfig {
264 ResolverConfig {
265 workspace_root: root.to_path_buf(),
266 path_mappings: Vec::new(),
267 search_roots: SCALA_SRC_DIRS.iter().map(|d| root.join(d)).collect(),
268 }
269 }
270
271 fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
272 let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
273 if ext != "scala" && ext != "sc" {
274 return Vec::new();
275 }
276 for search_root in &cfg.search_roots {
277 if let Ok(rel) = file.strip_prefix(search_root) {
278 let rel_str = rel
279 .to_str()
280 .unwrap_or("")
281 .trim_end_matches(".scala")
282 .trim_end_matches(".sc")
283 .replace(['/', '\\'], ".");
284 if !rel_str.is_empty() {
285 return vec![ModuleId {
286 canonical_path: rel_str,
287 }];
288 }
289 }
290 }
291 Vec::new()
292 }
293
294 fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
295 let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
296 if ext != "scala" && ext != "sc" {
297 return Resolution::NotApplicable;
298 }
299 let raw = &spec.raw;
300 let path_part = raw.replace('.', "/");
301 let exported_name = raw.rsplit('.').next().unwrap_or(raw).to_string();
302 for search_root in &cfg.search_roots {
303 let candidate = search_root.join(format!("{}.scala", path_part));
304 if candidate.exists() {
305 return Resolution::Resolved(candidate, exported_name.clone());
306 }
307 let candidate = search_root.join(format!("{}.sc", path_part));
308 if candidate.exists() {
309 return Resolution::Resolved(candidate, exported_name.clone());
310 }
311 }
312 Resolution::NotFound
313 }
314}
315
316fn extract_scaladoc(node: &Node, content: &str) -> Option<String> {
320 let mut prev = node.prev_sibling();
321 while let Some(sibling) = prev {
322 match sibling.kind() {
323 "block_comment" => {
324 let text = &content[sibling.byte_range()];
325 if text.starts_with("/**") {
326 return Some(clean_block_doc_comment(text));
327 }
328 return None;
329 }
330 "annotation" => {
331 }
333 _ => return None,
334 }
335 prev = sibling.prev_sibling();
336 }
337 None
338}
339
340fn clean_block_doc_comment(text: &str) -> String {
342 let lines: Vec<&str> = text
343 .strip_prefix("/**")
344 .unwrap_or(text)
345 .strip_suffix("*/")
346 .unwrap_or(text)
347 .lines()
348 .map(|l| l.trim().strip_prefix('*').unwrap_or(l).trim())
349 .filter(|l| !l.is_empty())
350 .collect();
351 lines.join(" ")
352}
353
354fn extract_scala_annotations(node: &Node, content: &str) -> Vec<String> {
359 let mut attrs = Vec::new();
360 let mut cursor = node.walk();
361 for child in node.children(&mut cursor) {
362 if child.kind() == "annotation" {
363 attrs.push(content[child.byte_range()].to_string());
364 }
365 }
366 attrs
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372 use crate::validate_unused_kinds_audit;
373
374 #[test]
375 fn unused_node_kinds_audit() {
376 #[rustfmt::skip]
377 let documented_unused: &[&str] = &[
378 "access_modifier", "access_qualifier", "arrow_renamed_identifier",
381 "as_renamed_identifier", "block_comment", "case_block", "case_class_pattern",
382 "class_parameter", "class_parameters", "derives_clause", "enum_body",
383 "enum_case_definitions", "enum_definition", "enumerator", "enumerators",
384 "export_declaration", "extends_clause", "extension_definition", "field_expression",
385 "full_enum_case", "identifier", "identifiers", "indented_block", "indented_cases",
386 "infix_modifier", "inline_modifier", "instance_expression", "into_modifier",
387 "macro_body", "modifiers", "name_and_type", "opaque_modifier", "open_modifier",
388 "operator_identifier", "package_clause", "package_identifier", "self_type",
389 "simple_enum_case", "template_body", "tracked_modifier", "transparent_modifier",
390 "val_declaration", "val_definition", "var_declaration", "var_definition",
391 "with_template_body",
392 "finally_clause", "type_case_clause",
394 "ascription_expression", "assignment_expression", "call_expression",
396 "generic_function", "interpolated_string_expression", "parenthesized_expression",
397 "postfix_expression", "prefix_expression", "quote_expression", "splice_expression",
398 "tuple_expression",
399 "annotated_type", "applied_constructor_type", "compound_type",
401 "contravariant_type_parameter", "covariant_type_parameter", "function_declaration",
402 "function_type", "generic_type", "given_definition", "infix_type", "lazy_parameter_type",
403 "literal_type", "match_type", "named_tuple_type", "parameter_types",
404 "projected_type", "repeated_parameter_type", "singleton_type", "stable_identifier",
405 "stable_type_identifier", "structural_type", "tuple_type", "type_arguments", "type_identifier", "type_lambda", "type_parameters", "typed_pattern",
406 "while_expression",
408 "match_expression",
409 "catch_clause",
410 "import_declaration",
411 "return_expression",
412 "if_expression",
413 "for_expression",
414 "throw_expression",
415 "block",
416 "infix_expression",
417 "case_clause",
418 "try_expression",
419 "do_while_expression",
420 "lambda_expression",
421 ];
422
423 validate_unused_kinds_audit(&Scala, documented_unused)
424 .expect("Scala unused node kinds audit failed");
425 }
426}