package languages import ( "fmt" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zzet/gortex/internal/graph" "github.com/zzet/gortex/internal/parser" sitter "github.com/zzet/gortex/internal/parser/tsitter" "github.com/zzet/gortex/internal/parser/tsitter/scala" ) func TestScalaAST_Debug(t *testing.T) { src := []byte(`import scala.collection.mutable trait Repository { def findById(id: String): Option[User] def save(user: User): Unit } class UserService(repo: Repository) { def getUser(id: String): Option[User] = { repo.findById(id) } } object Main { def main(args: Array[String]): Unit = { println("Hello") } } case class User(name: String, email: String) `) lang := scala.GetLanguage() tree, err := parser.ParseFile(src, lang) require.NoError(t, err) defer tree.Close() root := tree.RootNode() var walk func(n *sitter.Node, depth int) walk = func(n *sitter.Node, depth int) { indent := "" for i := 0; i < depth; i++ { indent += " " } if n.IsNamed() { t.Logf("%s%s [%d:%d - %d:%d] %q", indent, n.Type(), n.StartPoint().Row, n.StartPoint().Column, n.EndPoint().Row, n.EndPoint().Column, truncate(n.Content(src), 80)) } for i := 0; i < int(n.ChildCount()); i++ { walk(n.Child(i), depth+1) } } walk(root, 0) } func TestScalaExtractor_ClassWithMethods(t *testing.T) { src := []byte(`class UserService(repo: Repository) { def getUser(id: String): Option[User] = { repo.findById(id) } def deleteUser(id: String): Unit = { repo.delete(id) } } `) e := NewScalaExtractor() result, err := e.Extract("UserService.scala", src) require.NoError(t, err) types := nodesOfKind(result.Nodes, graph.KindType) require.Len(t, types, 1) assert.Equal(t, "UserService", types[0].Name) methods := nodesOfKind(result.Nodes, graph.KindMethod) require.Len(t, methods, 2) names := []string{methods[0].Name, methods[1].Name} assert.Contains(t, names, "getUser") assert.Contains(t, names, "deleteUser") memberEdges := edgesOfKind(result.Edges, graph.EdgeMemberOf) require.Len(t, memberEdges, 2) for _, edge := range memberEdges { assert.Equal(t, "UserService.scala::UserService", edge.To) } } func TestScalaExtractor_Trait(t *testing.T) { src := []byte(`trait Repository { def findById(id: String): Option[User] def save(user: User): Unit } `) e := NewScalaExtractor() result, err := e.Extract("Repository.scala", src) require.NoError(t, err) ifaces := nodesOfKind(result.Nodes, graph.KindInterface) require.Len(t, ifaces, 1, "expected 1 interface (trait), got %d", len(ifaces)) assert.Equal(t, "Repository", ifaces[0].Name) // Trait should have method names in Meta. meta := ifaces[0].Meta require.NotNil(t, meta) methodNames, ok := meta["methods"] require.True(t, ok, "expected Meta[\"methods\"] on trait") methodList, ok := methodNames.([]string) require.True(t, ok) assert.Contains(t, methodList, "findById") assert.Contains(t, methodList, "save") } func TestScalaExtractor_Object(t *testing.T) { src := []byte(`object Main { def main(args: Array[String]): Unit = { println("Hello") } def helper(): Int = 42 } `) e := NewScalaExtractor() result, err := e.Extract("Main.scala", src) require.NoError(t, err) types := nodesOfKind(result.Nodes, graph.KindType) require.Len(t, types, 1) assert.Equal(t, "Main", types[0].Name) methods := nodesOfKind(result.Nodes, graph.KindMethod) require.Len(t, methods, 2) names := []string{methods[0].Name, methods[1].Name} assert.Contains(t, names, "main") assert.Contains(t, names, "helper") memberEdges := edgesOfKind(result.Edges, graph.EdgeMemberOf) require.Len(t, memberEdges, 2) for _, edge := range memberEdges { assert.Equal(t, "Main.scala::Main", edge.To) } } func TestScalaExtractor_Imports(t *testing.T) { src := []byte(`import scala.collection.mutable import java.util.UUID import com.example.service.UserService object App { def run(): Unit = {} } `) e := NewScalaExtractor() result, err := e.Extract("App.scala", src) require.NoError(t, err) imports := edgesOfKind(result.Edges, graph.EdgeImports) require.Len(t, imports, 3) } func TestScalaExtractor_CaseClass(t *testing.T) { src := []byte(`case class User(name: String, email: String) `) e := NewScalaExtractor() result, err := e.Extract("User.scala", src) require.NoError(t, err) types := nodesOfKind(result.Nodes, graph.KindType) require.Len(t, types, 1) assert.Equal(t, "User", types[0].Name) } func TestScalaExtractor_CallSites(t *testing.T) { src := []byte(`object Main { def main(args: Array[String]): Unit = { println("Hello") greet("world") } def greet(name: String): Unit = { println(name) } } `) e := NewScalaExtractor() result, err := e.Extract("Main.scala", src) require.NoError(t, err) calls := edgesOfKind(result.Edges, graph.EdgeCalls) assert.GreaterOrEqual(t, len(calls), 2, "expected at least 2 call edges") targets := make(map[string]bool) for _, c := range calls { targets[c.To] = true } assert.True(t, targets["unresolved::*.println"], "missing println call") assert.True(t, targets["unresolved::*.greet"], "missing greet call") } // Unused import to suppress compiler error for fmt if needed. var _ = fmt.Sprint // TestScalaExtensionAndPackageScope is part of the C9 set: a Scala 3 extension // method attributes to its receiver type, and package scope is stamped. func TestScalaExtensionAndPackageScope(t *testing.T) { src := []byte("package com.app.util\n" + "extension (s: String)\n" + " def shout: String = s.toUpperCase\n") res, err := NewScalaExtractor().Extract("e.scala", src) require.NoError(t, err) var shout *graph.Node for _, n := range res.Nodes { if n.Name == "shout" { shout = n } } require.NotNil(t, shout, "extension method should be a node") assert.Equal(t, graph.KindMethod, shout.Kind) assert.Equal(t, "String", shout.Meta["receiver"], "attributed to the extended type") assert.Equal(t, true, shout.Meta["extension"]) assert.Equal(t, "com.app.util", shout.Meta["scope_pkg"]) } func TestScalaExtractor_FactoryChainReceiver(t *testing.T) { src := []byte("object M {\n" + " def builder(): Widget = new Widget()\n" + " def run() = {\n" + " builder().withX().build()\n" + " }\n" + "}\n") res, err := NewScalaExtractor().Extract("w.scala", src) if err != nil { t.Fatal(err) } var withX, build *graph.Edge for _, e := range res.Edges { if e.Kind != graph.EdgeCalls { continue } switch e.To { case "unresolved::*.withX": withX = e case "unresolved::*.build": build = e } } if withX == nil || build == nil { t.Fatal("withX()/build() call edges not found") } if withX.Meta["receiver_type"] != "Widget" { t.Errorf("withX receiver_type = %v, want Widget (builder() factory)", withX.Meta["receiver_type"]) } if got, _ := build.Meta["receiver_expr"].(string); got != "builder().withX()" { t.Errorf("build receiver_expr = %q, want builder().withX()", got) } }