Skip to main content

normalize_languages/
scala.rs

1//! Scala language support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8/// Scala language support.
9pub 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 has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &["class_definition", "object_definition", "trait_definition"]
28    }
29    fn function_kinds(&self) -> &'static [&'static str] {
30        &["function_definition"]
31    }
32    fn type_kinds(&self) -> &'static [&'static str] {
33        &["class_definition", "trait_definition"]
34    }
35    fn import_kinds(&self) -> &'static [&'static str] {
36        &["import_declaration"]
37    }
38
39    fn public_symbol_kinds(&self) -> &'static [&'static str] {
40        &[
41            "class_definition",
42            "object_definition",
43            "trait_definition",
44            "function_definition",
45        ]
46    }
47
48    fn visibility_mechanism(&self) -> VisibilityMechanism {
49        VisibilityMechanism::AccessModifier
50    }
51
52    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
53        // Scala: public by default, check for private/protected modifiers
54        // TODO: implement proper visibility checking for Scala
55        let name = match self.node_name(node, content) {
56            Some(n) => n.to_string(),
57            None => return Vec::new(),
58        };
59
60        let kind = match node.kind() {
61            "class_definition" => SymbolKind::Class,
62            "object_definition" => SymbolKind::Module,
63            "trait_definition" => SymbolKind::Trait,
64            "function_definition" => SymbolKind::Function,
65            _ => return Vec::new(),
66        };
67
68        vec![Export {
69            name,
70            kind,
71            line: node.start_position().row + 1,
72        }]
73    }
74
75    fn scope_creating_kinds(&self) -> &'static [&'static str] {
76        &["for_expression", "block", "lambda_expression"]
77    }
78
79    fn control_flow_kinds(&self) -> &'static [&'static str] {
80        &[
81            "if_expression",
82            "match_expression",
83            "for_expression",
84            "while_expression",
85            "do_while_expression",
86            "try_expression",
87            "return_expression",
88            "throw_expression",
89        ]
90    }
91
92    fn complexity_nodes(&self) -> &'static [&'static str] {
93        &[
94            "if_expression",
95            "match_expression",
96            "case_clause",
97            "for_expression",
98            "while_expression",
99            "do_while_expression",
100            "try_expression",
101            "catch_clause",
102            "infix_expression", // for && and ||
103        ]
104    }
105
106    fn nesting_nodes(&self) -> &'static [&'static str] {
107        &[
108            "if_expression",
109            "match_expression",
110            "for_expression",
111            "while_expression",
112            "do_while_expression",
113            "try_expression",
114            "function_definition",
115            "class_definition",
116            "object_definition",
117            "trait_definition",
118            "block",
119        ]
120    }
121
122    fn signature_suffix(&self) -> &'static str {
123        " {}"
124    }
125
126    fn extract_function(&self, node: &Node, content: &str, in_container: bool) -> Option<Symbol> {
127        let name = self.node_name(node, content)?;
128        let params = node
129            .child_by_field_name("parameters")
130            .map(|p| content[p.byte_range()].to_string())
131            .unwrap_or_else(|| "()".to_string());
132        let ret = node
133            .child_by_field_name("return_type")
134            .map(|r| format!(": {}", &content[r.byte_range()]))
135            .unwrap_or_default();
136
137        Some(Symbol {
138            name: name.to_string(),
139            kind: if in_container {
140                SymbolKind::Method
141            } else {
142                SymbolKind::Function
143            },
144            signature: format!("def {}{}{}", name, params, ret),
145            docstring: None,
146            attributes: Vec::new(),
147            start_line: node.start_position().row + 1,
148            end_line: node.end_position().row + 1,
149            visibility: Visibility::Public,
150            children: Vec::new(),
151            is_interface_impl: false,
152            implements: Vec::new(),
153        })
154    }
155
156    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
157        let name = self.node_name(node, content)?;
158        let (kind, keyword) = match node.kind() {
159            "object_definition" => (SymbolKind::Module, "object"),
160            "trait_definition" => (SymbolKind::Trait, "trait"),
161            _ => (SymbolKind::Class, "class"),
162        };
163
164        Some(Symbol {
165            name: name.to_string(),
166            kind,
167            signature: format!("{} {}", keyword, name),
168            docstring: None,
169            attributes: Vec::new(),
170            start_line: node.start_position().row + 1,
171            end_line: node.end_position().row + 1,
172            visibility: Visibility::Public,
173            children: Vec::new(),
174            is_interface_impl: false,
175            implements: Vec::new(),
176        })
177    }
178
179    fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
180        self.extract_container(node, content)
181    }
182
183    fn extract_docstring(&self, _node: &Node, _content: &str) -> Option<String> {
184        None
185    }
186
187    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
188        Vec::new()
189    }
190    fn extract_imports(&self, _node: &Node, _content: &str) -> Vec<Import> {
191        Vec::new()
192    }
193
194    fn format_import(&self, import: &Import, names: Option<&[&str]>) -> String {
195        // Scala: import pkg.Class or import pkg.{A, B, C}
196        let names_to_use: Vec<&str> = names
197            .map(|n| n.to_vec())
198            .unwrap_or_else(|| import.names.iter().map(|s| s.as_str()).collect());
199        if import.is_wildcard {
200            format!("import {}._", import.module)
201        } else if names_to_use.is_empty() {
202            format!("import {}", import.module)
203        } else if names_to_use.len() == 1 {
204            format!("import {}.{}", import.module, names_to_use[0])
205        } else {
206            format!("import {}.{{{}}}", import.module, names_to_use.join(", "))
207        }
208    }
209
210    fn is_public(&self, _node: &Node, _content: &str) -> bool {
211        true
212    }
213    fn get_visibility(&self, _node: &Node, _content: &str) -> Visibility {
214        Visibility::Public
215    }
216
217    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
218        {
219            let has_test_attr = symbol.attributes.iter().any(|a| a.contains("@Test"));
220            if has_test_attr {
221                return true;
222            }
223            match symbol.kind {
224                crate::SymbolKind::Class => {
225                    symbol.name.starts_with("Test") || symbol.name.ends_with("Test")
226                }
227                _ => false,
228            }
229        }
230    }
231
232    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
233        None
234    }
235
236    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
237        node.child_by_field_name("body")
238    }
239    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
240        false
241    }
242
243    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
244        let name_node = node.child_by_field_name("name")?;
245        Some(&content[name_node.byte_range()])
246    }
247
248    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
249        let ext = path.extension()?.to_str()?;
250        if !["scala", "sc"].contains(&ext) {
251            return None;
252        }
253        Some(path.to_string_lossy().to_string())
254    }
255    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
256        vec![format!("{}.scala", module)]
257    }
258
259    fn lang_key(&self) -> &'static str {
260        "scala"
261    }
262    fn resolve_local_import(&self, _: &str, _: &Path, _: &Path) -> Option<PathBuf> {
263        None
264    }
265    fn resolve_external_import(&self, _: &str, _: &Path) -> Option<ResolvedPackage> {
266        None
267    }
268    fn is_stdlib_import(&self, _: &str, _: &Path) -> bool {
269        false
270    }
271    fn get_version(&self, _: &Path) -> Option<String> {
272        None
273    }
274    fn find_package_cache(&self, _: &Path) -> Option<PathBuf> {
275        None
276    }
277    fn indexable_extensions(&self) -> &'static [&'static str] {
278        &["scala", "sc"]
279    }
280    fn find_stdlib(&self, _: &Path) -> Option<PathBuf> {
281        None
282    }
283    fn package_module_name(&self, name: &str) -> String {
284        name.strip_suffix(".scala").unwrap_or(name).to_string()
285    }
286    fn package_sources(&self, _: &Path) -> Vec<crate::PackageSource> {
287        Vec::new()
288    }
289    fn discover_packages(&self, _: &crate::PackageSource) -> Vec<(String, PathBuf)> {
290        Vec::new()
291    }
292    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
293        if path.is_file() {
294            Some(path.to_path_buf())
295        } else {
296            None
297        }
298    }
299
300    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
301        use crate::traits::{has_extension, skip_dotfiles};
302        if skip_dotfiles(name) {
303            return true;
304        }
305        !is_dir && !has_extension(name, self.indexable_extensions())
306    }
307}
308
309#[cfg(test)]
310mod tests {
311    use super::*;
312    use crate::validate_unused_kinds_audit;
313
314    #[test]
315    fn unused_node_kinds_audit() {
316        #[rustfmt::skip]
317        let documented_unused: &[&str] = &[
318            // STRUCTURAL
319            "access_modifier", "access_qualifier", "arrow_renamed_identifier",
320            "as_renamed_identifier", "block_comment", "case_block", "case_class_pattern",
321            "class_parameter", "class_parameters", "derives_clause", "enum_body",
322            "enum_case_definitions", "enum_definition", "enumerator", "enumerators",
323            "export_declaration", "extends_clause", "extension_definition", "field_expression",
324            "full_enum_case", "identifier", "identifiers", "indented_block", "indented_cases",
325            "infix_modifier", "inline_modifier", "instance_expression", "into_modifier",
326            "macro_body", "modifiers", "name_and_type", "opaque_modifier", "open_modifier",
327            "operator_identifier", "package_clause", "package_identifier", "self_type",
328            "simple_enum_case", "template_body", "tracked_modifier", "transparent_modifier",
329            "val_declaration", "val_definition", "var_declaration", "var_definition",
330            "with_template_body",
331            // CLAUSE
332            "finally_clause", "type_case_clause",
333            // EXPRESSION
334            "ascription_expression", "assignment_expression", "call_expression",
335            "generic_function", "interpolated_string_expression", "parenthesized_expression",
336            "postfix_expression", "prefix_expression", "quote_expression", "splice_expression",
337            "tuple_expression",
338            // TYPE
339            "annotated_type", "applied_constructor_type", "compound_type",
340            "contravariant_type_parameter", "covariant_type_parameter", "function_declaration",
341            "function_type", "generic_type", "given_definition", "infix_type", "lazy_parameter_type",
342            "literal_type", "match_type", "named_tuple_type", "parameter_types",
343            "projected_type", "repeated_parameter_type", "singleton_type", "stable_identifier",
344            "stable_type_identifier", "structural_type", "tuple_type", "type_arguments",
345            "type_definition", "type_identifier", "type_lambda", "type_parameters", "typed_pattern",
346        ];
347
348        validate_unused_kinds_audit(&Scala, documented_unused)
349            .expect("Scala unused node kinds audit failed");
350    }
351}