use tree_sitter::{Node, Parser};
use crate::error::{CodegraphError, Result};
use crate::graph::types::{
Binding, BindingKind, ByteSpan, EntryPoint, FileFacts, RefRole, Reference, Scope, ScopeId,
ScopeKind, Symbol, SymbolKind, TypeRefContext, Visibility,
};
use crate::lang::Language;
use crate::symbol::Descriptor;
use super::{
ExtractCtx, Extractor, MIN_REF_LEN, attach_reference_scopes, collect_call_references,
definition_bindings, field_text, import_bindings, innermost_scope, make_symbol, node_span,
node_text, one_line_signature, push_binding, push_import_ref, push_ref, push_scope,
push_type_ref, simple_type_name,
};
const CALL_QUERY: &str = r#"
[
(call_expression function: (identifier) @callee)
(call_expression function: (generic_function function: (identifier) @callee))
(call_expression function: (field_expression value: (_) @qualifier field: (identifier) @callee))
(call_expression function: (generic_function function: (field_expression value: (_) @qualifier field: (identifier) @callee)))
]
"#;
pub struct ScalaExtractor;
impl Extractor for ScalaExtractor {
fn lang(&self) -> Language {
Language::Scala
}
fn extract(&self, source: &str, file: &str) -> Result<FileFacts> {
let ts_language = crate::grammar::scala();
let mut parser = Parser::new();
parser
.set_language(&ts_language)
.map_err(|_| CodegraphError::Parse {
path: file.to_owned(),
})?;
let tree = parser
.parse(source, None)
.ok_or_else(|| CodegraphError::Parse {
path: file.to_owned(),
})?;
let root = tree.root_node();
let bytes = source.as_bytes();
let ctx = ExtractCtx {
bytes,
file,
lang: Language::Scala,
};
let namespaces = scala_namespaces(&root, bytes, file);
let defs = collect_symbols(&root, &ctx, &namespaces);
let def_bindings = definition_bindings(&defs);
let mut symbols = defs;
let mod_sym = super::module_symbol(Language::Scala, &namespaces, file, source.len());
let module_id = mod_sym.id.to_scip_string();
symbols.push(mod_sym);
let mut references = collect_call_references(
&root,
&ts_language,
CALL_QUERY,
Language::Scala,
bytes,
file,
)?;
collect_inheritance(&root, bytes, file, &mut references);
collect_imports(&root, bytes, file, &mut references, &module_id);
collect_type_references(&root, bytes, file, &mut references);
collect_read_references(&root, bytes, file, &mut references);
collect_write_references(&root, bytes, file, &mut references);
let scopes = collect_scopes(&root, source.len());
attach_reference_scopes(&mut references, &scopes);
let mut bindings = collect_bindings(&root, bytes, &scopes);
bindings.extend(def_bindings);
bindings.extend(import_bindings(&references, &scopes));
Ok(FileFacts {
file: file.to_owned(),
lang: Language::Scala.as_str().to_owned(),
symbols,
references,
scopes,
bindings,
ffi_exports: Vec::new(),
})
}
}
fn scala_namespaces(root: &Node, bytes: &[u8], file: &str) -> Vec<String> {
let mut segments: Vec<String> = Vec::new();
for child in root.children(&mut root.walk()) {
if child.kind() != "package_clause" {
continue;
}
if let Some(name_node) = child.child_by_field_name("name") {
let text = node_text(&name_node, bytes);
for seg in text.split('.').filter(|s| !s.is_empty()) {
segments.push(seg.to_owned());
}
}
}
if !segments.is_empty() {
return segments;
}
let p = file
.strip_suffix(".scala")
.or_else(|| file.strip_suffix(".sc"))
.unwrap_or(file);
let p = p.strip_prefix("src/").unwrap_or(p);
p.split('/')
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
}
fn collect_symbols(root: &Node, ctx: &ExtractCtx, namespaces: &[String]) -> Vec<Symbol> {
let ns_descriptors: Vec<Descriptor> = namespaces
.iter()
.cloned()
.map(Descriptor::Namespace)
.collect();
let mut out = Vec::new();
collect_defs_in(root, ctx, &ns_descriptors, &mut out);
out
}
fn collect_defs_in(node: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
for child in node.children(&mut node.walk()) {
match child.kind() {
"package_clause" => {
if let Some(body) = child.child_by_field_name("body") {
collect_defs_in(&body, ctx, prefix, out);
} else {
}
}
"class_definition" => {
emit_type_def(&child, ctx, prefix, SymbolKind::Class, out);
}
"trait_definition" => {
emit_type_def(&child, ctx, prefix, SymbolKind::Trait, out);
}
"object_definition" | "package_object" => {
emit_type_def(&child, ctx, prefix, SymbolKind::Module, out);
}
"enum_definition" => {
emit_enum_def(&child, ctx, prefix, out);
}
"function_definition" => {
emit_function(&child, ctx, prefix, out);
}
"val_definition" => {
emit_val_or_var(&child, ctx, prefix, SymbolKind::Const, out);
}
"var_definition" => {
emit_val_or_var(&child, ctx, prefix, SymbolKind::Static, out);
}
"type_definition" => {
emit_type_alias(&child, ctx, prefix, out);
}
_ => {
collect_defs_in(&child, ctx, prefix, out);
}
}
}
}
fn emit_type_def(
node: &Node,
ctx: &ExtractCtx,
prefix: &[Descriptor],
kind: SymbolKind,
out: &mut Vec<Symbol>,
) {
let Some(name) = name_text(node, ctx.bytes) else {
return;
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Type(name.clone()));
out.push(make_symbol(
ctx,
node,
name,
kind,
read_visibility(node, ctx.bytes),
descriptors.clone(),
one_line_signature(node_text(node, ctx.bytes), &['{', ';']),
));
if let Some(body) = node.child_by_field_name("body") {
collect_members_in(&body, ctx, &descriptors, out);
}
}
fn emit_enum_def(node: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
let Some(name) = name_text(node, ctx.bytes) else {
return;
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Type(name.clone()));
out.push(make_symbol(
ctx,
node,
name,
SymbolKind::Enum,
read_visibility(node, ctx.bytes),
descriptors.clone(),
one_line_signature(node_text(node, ctx.bytes), &['{', ';']),
));
if let Some(body) = node.child_by_field_name("body") {
for group in body.children(&mut body.walk()) {
if group.kind() != "enum_case_definitions" {
continue;
}
for case in group.children(&mut group.walk()) {
if !matches!(case.kind(), "simple_enum_case" | "full_enum_case") {
continue;
}
let mut cursor = case.walk();
for name_node in case
.children_by_field_name("name", &mut cursor)
.filter(|n| matches!(n.kind(), "identifier" | "operator_identifier"))
{
let case_name = node_text(&name_node, ctx.bytes).to_owned();
let mut case_desc = descriptors.clone();
case_desc.push(Descriptor::Term(case_name.clone()));
out.push(make_symbol(
ctx,
&case,
case_name,
SymbolKind::Const,
read_visibility(&case, ctx.bytes),
case_desc,
one_line_signature(node_text(&case, ctx.bytes), &['{', ';', ',']),
));
}
}
}
collect_members_in(&body, ctx, &descriptors, out);
}
}
fn collect_members_in(
body: &Node,
ctx: &ExtractCtx,
type_prefix: &[Descriptor],
out: &mut Vec<Symbol>,
) {
for member in body.children(&mut body.walk()) {
match member.kind() {
"function_definition" => {
emit_function(&member, ctx, type_prefix, out);
}
"val_definition" => {
emit_val_or_var(&member, ctx, type_prefix, SymbolKind::Const, out);
}
"var_definition" => {
emit_val_or_var(&member, ctx, type_prefix, SymbolKind::Static, out);
}
"type_definition" => {
emit_type_alias(&member, ctx, type_prefix, out);
}
"class_definition" => {
emit_type_def(&member, ctx, type_prefix, SymbolKind::Class, out);
}
"trait_definition" => {
emit_type_def(&member, ctx, type_prefix, SymbolKind::Trait, out);
}
"object_definition" => {
emit_type_def(&member, ctx, type_prefix, SymbolKind::Module, out);
}
"enum_definition" => {
emit_enum_def(&member, ctx, type_prefix, out);
}
_ => {}
}
}
}
fn emit_function(node: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
let Some(name) = name_text(node, ctx.bytes) else {
return;
};
let is_main = name == "main";
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Method {
name: name.clone(),
disambiguator: String::new(),
});
out.push(make_symbol(
ctx,
node,
name,
SymbolKind::Method,
read_visibility(node, ctx.bytes),
descriptors,
one_line_signature(node_text(node, ctx.bytes), &['{', ';', '=']),
));
if is_main {
if let Some(s) = out.last_mut() {
s.entry_points.push(EntryPoint::Main);
}
}
}
fn emit_val_or_var(
node: &Node,
ctx: &ExtractCtx,
prefix: &[Descriptor],
kind: SymbolKind,
out: &mut Vec<Symbol>,
) {
let name: Option<String> = if let Some(pat) = node.child_by_field_name("pattern") {
if pat.kind() == "identifier" {
Some(node_text(&pat, ctx.bytes).to_owned())
} else {
None
}
} else {
field_text(node, "name", ctx.bytes)
};
let Some(name) = name else { return };
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Term(name.clone()));
out.push(make_symbol(
ctx,
node,
name,
kind,
read_visibility(node, ctx.bytes),
descriptors,
one_line_signature(node_text(node, ctx.bytes), &['{', ';', '=']),
));
}
fn emit_type_alias(node: &Node, ctx: &ExtractCtx, prefix: &[Descriptor], out: &mut Vec<Symbol>) {
let Some(name) = field_text(node, "name", ctx.bytes) else {
return;
};
let mut descriptors = prefix.to_vec();
descriptors.push(Descriptor::Type(name.clone()));
out.push(make_symbol(
ctx,
node,
name,
SymbolKind::TypeAlias,
read_visibility(node, ctx.bytes),
descriptors,
one_line_signature(node_text(node, ctx.bytes), &['{', ';', '=']),
));
}
fn name_text(node: &Node, bytes: &[u8]) -> Option<String> {
let n = node.child_by_field_name("name")?;
Some(node_text(&n, bytes).to_owned())
}
fn read_visibility(node: &Node, bytes: &[u8]) -> Visibility {
let modifiers = node
.children(&mut node.walk())
.find(|c| c.kind() == "modifiers");
let modifiers = match modifiers {
Some(m) => m,
None => return Visibility::Public,
};
let access_mod = modifiers
.children(&mut modifiers.walk())
.find(|c| c.kind() == "access_modifier");
let access_mod = match access_mod {
Some(a) => a,
None => return Visibility::Public,
};
let keyword_node = match access_mod.child(0) {
Some(n) => n,
None => return Visibility::Public,
};
let keyword = node_text(&keyword_node, bytes);
let has_qualifier = access_mod
.children(&mut access_mod.walk())
.any(|c| c.kind() == "access_qualifier");
if has_qualifier {
Visibility::Internal
} else {
match keyword {
"private" => Visibility::Private,
"protected" => Visibility::Protected,
_ => Visibility::Public,
}
}
}
fn collect_inheritance(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "extends_clause" {
for child in node.named_children(&mut node.walk()) {
match child.kind() {
"type_identifier" | "identifier" => {
push_ref(
out,
node_text(&child, bytes),
&child,
file,
RefRole::IsImplementation,
);
}
"generic_type" | "stable_type_identifier" => {
let name = simple_type_name(node_text(&child, bytes), ".");
push_ref(out, name, &child, file, RefRole::IsImplementation);
}
_ => {}
}
}
}
for child in node.children(&mut node.walk()) {
collect_inheritance(&child, bytes, file, out);
}
}
fn collect_imports(
node: &Node,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
module_id: &str,
) {
if node.kind() == "import_declaration" {
collect_import_node(node, bytes, file, out, module_id);
return;
}
for child in node.children(&mut node.walk()) {
collect_imports(&child, bytes, file, out, module_id);
}
}
fn collect_import_node(
node: &Node,
bytes: &[u8],
file: &str,
out: &mut Vec<Reference>,
module_id: &str,
) {
let mut prefix_parts: Vec<&str> = Vec::new();
let mut selector_node: Option<Node> = None;
let mut last_id: Option<Node> = None;
for child in node.named_children(&mut node.walk()) {
match child.kind() {
"identifier" | "operator_identifier" => {
if let Some(prev) = last_id.take() {
prefix_parts.push(node_text(&prev, bytes));
}
last_id = Some(child);
}
"import_selectors" => {
selector_node = Some(child);
}
_ => {}
}
}
if let Some(sel) = selector_node {
if let Some(last) = last_id {
prefix_parts.push(node_text(&last, bytes));
}
let from_path = prefix_parts.join(".");
for sel_child in sel.named_children(&mut sel.walk()) {
match sel_child.kind() {
"identifier" => {
let name = node_text(&sel_child, bytes);
if name == "_" || name == "*" {
continue;
}
push_import_ref(out, name, &sel_child, file, module_id, &from_path);
}
"import_selector" => {
if let Some(first) = sel_child.named_children(&mut sel_child.walk()).next() {
if first.kind() == "identifier" {
let name = node_text(&first, bytes);
if name == "_" || name == "*" {
continue;
}
let has_rename = sel_child
.children(&mut sel_child.walk())
.any(|c| c.kind() == "=>");
if has_rename {
continue; }
push_import_ref(out, name, &first, file, module_id, &from_path);
}
}
}
_ => {}
}
}
} else if let Some(leaf) = last_id {
let leaf_text = node_text(&leaf, bytes);
if leaf_text == "_" || leaf_text == "*" {
return; }
let from_path = prefix_parts.join(".");
push_import_ref(out, leaf_text, &leaf, file, module_id, &from_path);
}
}
fn is_non_read_position(node: &Node) -> bool {
let parent = match node.parent() {
Some(p) => p,
None => return true, };
match parent.kind() {
"call_expression" => parent.child_by_field_name("function").as_ref() == Some(node),
"generic_function" => parent.child_by_field_name("function").as_ref() == Some(node),
"function_definition"
| "class_definition"
| "trait_definition"
| "object_definition"
| "package_object"
| "enum_definition"
| "type_definition" => parent.child_by_field_name("name").as_ref() == Some(node),
"val_definition" | "var_definition" => {
parent.child_by_field_name("pattern").as_ref() == Some(node)
}
"parameter" | "class_parameter" => {
parent.child_by_field_name("name").as_ref() == Some(node)
}
"import_declaration" | "import_selectors" | "import_selector" => true,
"field_expression" => parent.child_by_field_name("field").as_ref() == Some(node),
"assignment_expression" => parent.child_by_field_name("left").as_ref() == Some(node),
_ => false,
}
}
fn collect_read_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "identifier" {
let name = node_text(node, bytes);
if name.len() >= MIN_REF_LEN && !is_non_read_position(node) {
push_ref(out, name, node, file, RefRole::Read);
}
return;
}
for child in node.children(&mut node.walk()) {
collect_read_references(&child, bytes, file, out);
}
}
fn collect_write_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
if node.kind() == "assignment_expression" {
if let Some(lhs) = node.child_by_field_name("left") {
if lhs.kind() == "identifier" {
let name = node_text(&lhs, bytes);
if name.len() >= MIN_REF_LEN {
push_ref(out, name, &lhs, file, RefRole::Write);
}
}
}
}
for child in node.children(&mut node.walk()) {
collect_write_references(&child, bytes, file, out);
}
}
fn collect_type_references(node: &Node, bytes: &[u8], file: &str, out: &mut Vec<Reference>) {
match node.kind() {
"function_definition" => {
if let Some(ret) = node.child_by_field_name("return_type") {
type_leaf(&ret, bytes, file, TypeRefContext::ReturnType, out);
}
}
"val_definition" | "var_definition" => {
if let Some(ty) = node.child_by_field_name("type") {
type_leaf(&ty, bytes, file, TypeRefContext::Field, out);
}
}
"parameter" | "class_parameter" => {
if let Some(ty) = node.child_by_field_name("type") {
type_leaf(&ty, bytes, file, TypeRefContext::ParameterType, out);
}
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_type_references(&child, bytes, file, out);
}
}
fn type_leaf(node: &Node, bytes: &[u8], file: &str, ctx: TypeRefContext, out: &mut Vec<Reference>) {
match node.kind() {
"unit_type" | "tuple_type" | "function_type" => {}
"type_identifier" | "identifier" => {
let name = node_text(node, bytes);
push_type_ref(out, name, node, file, ctx);
}
"stable_type_identifier" | "generic_type" => {
let name = simple_type_name(node_text(node, bytes), ".");
push_type_ref(out, name, node, file, ctx);
}
_ => {
for child in node.named_children(&mut node.walk()) {
type_leaf(&child, bytes, file, ctx, out);
}
}
}
}
fn collect_scopes(root: &Node, source_len: usize) -> Vec<Scope> {
let mut scopes = Vec::new();
push_scope(
&mut scopes,
None,
ByteSpan {
start: 0,
end: source_len,
},
ScopeKind::Module,
);
for child in root.children(&mut root.walk()) {
scope_dfs(&child, 0, &mut scopes);
}
scopes
}
fn scope_dfs(node: &Node, parent_id: ScopeId, scopes: &mut Vec<Scope>) {
match node.kind() {
"class_definition" | "trait_definition" | "object_definition" | "package_object"
| "enum_definition" => {
let type_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Type);
if let Some(body) = node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, type_id, scopes);
}
}
}
"function_definition" => {
let fn_id = push_scope(
scopes,
Some(parent_id),
node_span(node),
ScopeKind::Function,
);
if let Some(body) = node.child_by_field_name("body") {
for child in body.children(&mut body.walk()) {
scope_dfs(&child, fn_id, scopes);
}
}
}
"block" => {
let block_id = push_scope(scopes, Some(parent_id), node_span(node), ScopeKind::Block);
for child in node.children(&mut node.walk()) {
scope_dfs(&child, block_id, scopes);
}
}
_ => {
for child in node.children(&mut node.walk()) {
scope_dfs(&child, parent_id, scopes);
}
}
}
}
fn collect_bindings(root: &Node, bytes: &[u8], scopes: &[Scope]) -> Vec<Binding> {
let mut out = Vec::new();
collect_bindings_dfs(root, bytes, scopes, &mut out);
out
}
fn collect_bindings_dfs(node: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
match node.kind() {
"function_definition" => {
for child in node.children(&mut node.walk()) {
if child.kind() == "parameters" || child.kind() == "parameter_clause" {
collect_params(&child, bytes, scopes, out);
}
}
}
"val_definition" | "var_definition" => {
if let Some(pat) = node.child_by_field_name("pattern") {
if pat.kind() == "identifier" {
let name = node_text(&pat, bytes);
let intro = pat.start_byte();
if name.len() >= MIN_REF_LEN && innermost_scope(intro, scopes) != Some(0) {
push_binding(out, name.to_owned(), intro, BindingKind::Local, scopes);
}
}
}
}
_ => {}
}
for child in node.children(&mut node.walk()) {
collect_bindings_dfs(&child, bytes, scopes, out);
}
}
fn collect_params(params: &Node, bytes: &[u8], scopes: &[Scope], out: &mut Vec<Binding>) {
for child in params.named_children(&mut params.walk()) {
if child.kind() == "parameter" || child.kind() == "class_parameter" {
let name_opt = child
.child_by_field_name("name")
.map(|n| node_text(&n, bytes).to_owned());
if let Some(name) = name_opt {
let intro = child.start_byte();
push_binding(out, name, intro, BindingKind::Param, scopes);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn extract(src: &str, file: &str) -> FileFacts {
ScalaExtractor.extract(src, file).unwrap()
}
fn by_name(facts: &FileFacts, name: &str) -> Option<Symbol> {
facts.symbols.iter().find(|s| s.name == name).cloned()
}
#[test]
fn main_method_is_entry_point() {
let facts = extract(
"object M { def main(args: Array[String]): Unit = {} }",
"src/M.scala",
);
let main = by_name(&facts, "main").unwrap();
assert!(
main.entry_points
.iter()
.any(|e| matches!(e, EntryPoint::Main))
);
}
#[test]
fn class_and_method_get_correct_scip_strings() {
let src = r#"
package com.example
class SessionManager {
def validate(token: String): Boolean = true
}
"#;
let facts = extract(src, "src/com/example/SessionManager.scala");
let sm = by_name(&facts, "SessionManager").unwrap();
assert_eq!(sm.kind, SymbolKind::Class);
assert_eq!(
sm.id.to_scip_string(),
"codegraph . . . com/example/SessionManager#"
);
let validate = by_name(&facts, "validate").unwrap();
assert_eq!(validate.kind, SymbolKind::Method);
assert_eq!(
validate.id.to_scip_string(),
"codegraph . . . com/example/SessionManager#validate()."
);
assert_eq!(facts.lang, "scala");
}
#[test]
fn package_declaration_yields_namespace_descriptors() {
let src = r#"
package a.b
class C {}
"#;
let facts = extract(src, "src/a/b/C.scala");
let c = by_name(&facts, "C").unwrap();
assert_eq!(c.id.to_scip_string(), "codegraph . . . a/b/C#");
}
#[test]
fn trait_is_extracted_as_trait_kind() {
let src = r#"
package io.example
trait Readable {
def read(): String
}
"#;
let facts = extract(src, "src/io/example/Readable.scala");
let t = by_name(&facts, "Readable").unwrap();
assert_eq!(t.kind, SymbolKind::Trait);
assert_eq!(
t.id.to_scip_string(),
"codegraph . . . io/example/Readable#"
);
}
#[test]
fn object_is_extracted_as_module_with_nested_member() {
let src = r#"
package app
object Config {
val host: String = "localhost"
}
"#;
let facts = extract(src, "src/app/Config.scala");
let obj = by_name(&facts, "Config").unwrap();
assert_eq!(obj.kind, SymbolKind::Module);
assert_eq!(obj.id.to_scip_string(), "codegraph . . . app/Config#");
let host = by_name(&facts, "host").unwrap();
assert_eq!(host.kind, SymbolKind::Const);
assert_eq!(host.id.to_scip_string(), "codegraph . . . app/Config#host.");
}
#[test]
fn scala3_enum_yields_enum_and_cases() {
let src = r#"
package colors
enum Color {
case Red
case Green
case Blue
}
"#;
let facts = extract(src, "src/colors/Color.scala");
let color = by_name(&facts, "Color").unwrap();
assert_eq!(color.kind, SymbolKind::Enum);
assert_eq!(color.id.to_scip_string(), "codegraph . . . colors/Color#");
let red = by_name(&facts, "Red").unwrap();
assert_eq!(red.kind, SymbolKind::Const);
assert_eq!(red.id.to_scip_string(), "codegraph . . . colors/Color#Red.");
}
#[test]
fn type_alias_is_extracted() {
let src = r#"
package myapp
type Id = Int
"#;
let facts = extract(src, "src/myapp/Types.scala");
let id = by_name(&facts, "Id").unwrap();
assert_eq!(id.kind, SymbolKind::TypeAlias);
assert_eq!(id.id.to_scip_string(), "codegraph . . . myapp/Id#");
}
#[test]
fn parameter_type_yields_type_reference() {
let src = r#"
package myapp
class Handler {
def run(req: Request): Unit = {}
}
"#;
let facts = extract(src, "src/myapp/Handler.scala");
assert!(
facts
.references
.iter()
.any(|r| r.role == RefRole::TypeRef && r.name == "Request"),
"expected a TypeRef to parameter type 'Request': {:?}",
facts
.references
.iter()
.map(|r| (&r.role, &r.name))
.collect::<Vec<_>>()
);
}
#[test]
fn qualified_call_captures_qualifier() {
let src = r#"
class Client {
def run(): Unit = {
val svc = new Service()
svc.process()
}
}
"#;
let facts = extract(src, "src/Client.scala");
let process = facts
.references
.iter()
.find(|r| r.name == "process")
.expect("expected Call ref for 'process'");
assert_eq!(process.role, RefRole::Call);
assert_eq!(
process.qualifier.as_deref(),
Some("svc"),
"expected qualifier 'svc' on the process call ref",
);
}
#[test]
fn import_produces_import_reference_with_from_path() {
let src = r#"
import scala.collection.mutable.ArrayBuffer
class Foo {}
"#;
let facts = extract(src, "src/Foo.scala");
let import_refs: Vec<&Reference> = facts
.references
.iter()
.filter(|r| r.role == RefRole::Import)
.collect();
let arr = import_refs
.iter()
.find(|r| r.name == "ArrayBuffer")
.expect("expected import ref for 'ArrayBuffer'");
assert_eq!(
arr.from_path,
Some("scala.collection.mutable".to_owned()),
"from_path should be 'scala.collection.mutable', got {:?}",
arr.from_path
);
}
#[test]
fn reassignment_emits_write_and_reads_for_rhs() {
let src = r#"
object O {
def m(): Unit = {
var total = 0
val bonus = 10
total = total + bonus
}
}
"#;
let facts = extract(src, "src/O.scala");
let refs = &facts.references;
assert!(
refs.iter()
.any(|r| r.role == RefRole::Write && r.name == "total"),
"expected Write ref for 'total': {:?}",
refs.iter().map(|r| (&r.role, &r.name)).collect::<Vec<_>>()
);
assert!(
refs.iter()
.any(|r| r.role == RefRole::Read && r.name == "bonus"),
"expected Read ref for 'bonus': {:?}",
refs.iter().map(|r| (&r.role, &r.name)).collect::<Vec<_>>()
);
let read_totals: Vec<_> = refs
.iter()
.filter(|r| r.role == RefRole::Read && r.name == "total")
.collect();
assert!(
!read_totals.is_empty(),
"expected at least one Read ref for 'total' (RHS usage)"
);
}
#[test]
fn val_definition_does_not_emit_write_for_binding_name() {
let src = r#"
object O {
def m(): Unit = {
val result = compute()
}
def compute(): Int = 42
}
"#;
let facts = extract(src, "src/O.scala");
assert!(
!facts
.references
.iter()
.any(|r| r.role == RefRole::Write && r.name == "result"),
"val binding 'result' must NOT emit a Write ref: {:?}",
facts
.references
.iter()
.map(|r| (&r.role, &r.name))
.collect::<Vec<_>>()
);
}
#[test]
fn free_call_arg_is_read_but_callee_is_not() {
let src = r#"
object O {
def m(): Unit = {
val config = Config()
logger(config)
}
def logger(x: Any): Unit = {}
}
"#;
let facts = extract(src, "src/O.scala");
assert!(
facts
.references
.iter()
.any(|r| r.role == RefRole::Read && r.name == "config"),
"expected Read ref for argument 'config': {:?}",
facts
.references
.iter()
.map(|r| (&r.role, &r.name))
.collect::<Vec<_>>()
);
assert!(
!facts
.references
.iter()
.any(|r| r.role == RefRole::Read && r.name == "logger"),
"callee 'logger' must NOT emit a Read ref: {:?}",
facts
.references
.iter()
.map(|r| (&r.role, &r.name))
.collect::<Vec<_>>()
);
}
#[test]
fn plain_def_has_public_visibility() {
let src = r#"
package app
class Svc {
def open(): Unit = {}
}
"#;
let facts = extract(src, "src/app/Svc.scala");
let sym = by_name(&facts, "open").unwrap();
assert_eq!(
sym.visibility,
Visibility::Public,
"plain `def` must be Public (Scala default)"
);
}
#[test]
fn private_def_has_private_visibility() {
let src = r#"
package app
class Svc {
private def secret(): Unit = {}
}
"#;
let facts = extract(src, "src/app/Svc.scala");
let sym = by_name(&facts, "secret").unwrap();
assert_eq!(
sym.visibility,
Visibility::Private,
"`private def` must be Private"
);
}
#[test]
fn protected_def_has_protected_visibility() {
let src = r#"
package app
class Svc {
protected def hook(): Unit = {}
}
"#;
let facts = extract(src, "src/app/Svc.scala");
let sym = by_name(&facts, "hook").unwrap();
assert_eq!(
sym.visibility,
Visibility::Protected,
"`protected def` must be Protected"
);
}
#[test]
fn private_qualified_def_has_internal_visibility() {
let src = r#"
package app
class Svc {
private[app] def pkg(): Unit = {}
}
"#;
let facts = extract(src, "src/app/Svc.scala");
let sym = by_name(&facts, "pkg").unwrap();
assert_eq!(
sym.visibility,
Visibility::Internal,
"`private[pkg] def` must be Internal"
);
}
#[test]
fn field_expression_receiver_is_read_but_leaf_is_not() {
let src = r#"
object O {
def m(): Unit = {
var value = 0
val source = Src()
value = source.field
}
class Src { val field: Int = 1 }
}
"#;
let facts = extract(src, "src/O.scala");
assert!(
facts
.references
.iter()
.any(|r| r.role == RefRole::Read && r.name == "source"),
"expected Read ref for receiver 'source': {:?}",
facts
.references
.iter()
.map(|r| (&r.role, &r.name))
.collect::<Vec<_>>()
);
assert!(
!facts
.references
.iter()
.any(|r| r.role == RefRole::Read && r.name == "field"),
"member-access leaf 'field' must NOT emit a Read ref: {:?}",
facts
.references
.iter()
.map(|r| (&r.role, &r.name))
.collect::<Vec<_>>()
);
}
}