Skip to main content

normalize_languages/
scala.rs

1//! Scala language support.
2
3use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
4use tree_sitter::Node;
5
6/// Scala language support.
7pub 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        // import pkg.Class or import pkg.{A, B} or import pkg._
110        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                // import pkg.{A, B, C}
117                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        // Scala: import pkg.Class or import pkg.{A, B, C}
161        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        // Scala uses `access_modifier` child with optional `access_qualifier`.
201        // `private` → Private, `protected` → Protected, no modifier → Public.
202        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
246/// Extract a ScalaDoc comment (`/** ... */`) preceding a node.
247///
248/// Walks backwards through siblings looking for a `block_comment` starting with `/**`.
249fn 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                // Skip annotations between doc comment and declaration
262            }
263            _ => return None,
264        }
265        prev = sibling.prev_sibling();
266    }
267    None
268}
269
270/// Clean a `/** ... */` block doc comment into plain text.
271fn 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
284/// Extract annotations from a Scala definition node.
285///
286/// Scala annotations (`@deprecated`, `@tailrec`, etc.) appear as `annotation`
287/// children of the definition node.
288fn 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            // STRUCTURAL
309            "access_modifier",     // used in get_visibility (audit matched by "if" substring)
310            "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            // CLAUSE
323            "finally_clause", "type_case_clause",
324            // EXPRESSION
325            "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            // TYPE
330            "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            // control flow — not extracted as symbols
337            "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}