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