Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

729 lines
20 KiB
Go

package languages
import (
"strings"
"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/kotlin"
)
// TestKotlinAST_Debug dumps the AST to verify node types used in queries.
func TestKotlinAST_Debug(t *testing.T) {
src := []byte(`package com.example
import kotlin.collections.List
interface Greeter {
fun greet(name: String): String
}
class HelloGreeter : Greeter {
override fun greet(name: String): String {
return "Hello, $name"
}
fun helper() {}
}
data class User(val name: String, val age: Int)
object Singleton {
fun instance(): Singleton = this
}
fun topLevel(): Int {
println("hello")
return 42
}
val VERSION = "1.0"
var counter = 0
`)
lang := kotlin.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), 60))
}
for i := 0; i < int(n.ChildCount()); i++ {
walk(n.Child(i), depth+1)
}
}
walk(root, 0)
}
func truncate(s string, max int) string {
if len(s) > max {
return s[:max] + "..."
}
return s
}
func TestKotlinExtractor_ClassWithMethods(t *testing.T) {
src := []byte(`class UserService {
fun getUser(id: String): User {
return findById(id)
}
fun deleteUser(id: String) {
remove(id)
}
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("UserService.kt", 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.kt::UserService", edge.To)
}
}
func TestKotlinExtractor_Interface(t *testing.T) {
src := []byte(`interface Repository {
fun findById(id: String): User
fun save(user: User)
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("Repository.kt", src)
require.NoError(t, err)
ifaces := nodesOfKind(result.Nodes, graph.KindInterface)
require.Len(t, ifaces, 1, "expected 1 interface, got %d", len(ifaces))
assert.Equal(t, "Repository", ifaces[0].Name)
}
func TestKotlinExtractor_EnumClass(t *testing.T) {
src := []byte(`enum class Direction {
NORTH, SOUTH, EAST, WEST
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("Direction.kt", src)
require.NoError(t, err)
types := nodesOfKind(result.Nodes, graph.KindType)
require.Len(t, types, 1)
assert.Equal(t, "Direction", types[0].Name)
require.NotNil(t, types[0].Meta, "enum should carry Meta[\"kind\"]=\"enum\"")
assert.Equal(t, "enum", types[0].Meta["kind"])
entries := map[string]bool{}
for _, n := range result.Nodes {
if n.Kind == graph.KindEnumMember {
entries[n.Name] = true
}
}
assert.Equal(t, map[string]bool{"NORTH": true, "SOUTH": true, "EAST": true, "WEST": true}, entries)
}
func TestKotlinExtractor_TopLevelFunction(t *testing.T) {
src := []byte(`fun greet(name: String): String {
println(name)
return "Hello, $name"
}
fun add(a: Int, b: Int): Int = a + b
`)
e := NewKotlinExtractor()
result, err := e.Extract("utils.kt", src)
require.NoError(t, err)
funcs := nodesOfKind(result.Nodes, graph.KindFunction)
require.Len(t, funcs, 2)
names := []string{funcs[0].Name, funcs[1].Name}
assert.Contains(t, names, "greet")
assert.Contains(t, names, "add")
// Should not be methods.
methods := nodesOfKind(result.Nodes, graph.KindMethod)
assert.Empty(t, methods)
}
func TestKotlinExtractor_Imports(t *testing.T) {
src := []byte(`import kotlin.collections.List
import com.example.service.UserService
import java.util.UUID
fun main() {}
`)
e := NewKotlinExtractor()
result, err := e.Extract("main.kt", src)
require.NoError(t, err)
imports := edgesOfKind(result.Edges, graph.EdgeImports)
require.Len(t, imports, 3)
}
func TestKotlinExtractor_DataClass(t *testing.T) {
src := []byte(`data class User(val name: String, val age: Int)
`)
e := NewKotlinExtractor()
result, err := e.Extract("User.kt", src)
require.NoError(t, err)
types := nodesOfKind(result.Nodes, graph.KindType)
require.Len(t, types, 1)
assert.Equal(t, "User", types[0].Name)
}
func TestKotlinExtractor_Object(t *testing.T) {
src := []byte(`object Singleton {
fun getInstance(): Singleton {
return this
}
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("Singleton.kt", src)
require.NoError(t, err)
types := nodesOfKind(result.Nodes, graph.KindType)
require.Len(t, types, 1)
assert.Equal(t, "Singleton", types[0].Name)
methods := nodesOfKind(result.Nodes, graph.KindMethod)
require.Len(t, methods, 1)
assert.Equal(t, "getInstance", methods[0].Name)
memberEdges := edgesOfKind(result.Edges, graph.EdgeMemberOf)
require.Len(t, memberEdges, 1)
assert.Equal(t, "Singleton.kt::Singleton", memberEdges[0].To)
}
func TestKotlinExtractor_TopLevelProperties(t *testing.T) {
src := []byte(`val VERSION = "1.0"
var counter = 0
class Foo {
val internal = "hidden"
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("config.kt", src)
require.NoError(t, err)
vars := nodesOfKind(result.Nodes, graph.KindVariable)
require.Len(t, vars, 2, "expected only top-level properties")
names := []string{vars[0].Name, vars[1].Name}
assert.Contains(t, names, "VERSION")
assert.Contains(t, names, "counter")
}
func TestKotlinExtractor_CallSites(t *testing.T) {
src := []byte(`fun main() {
println("hello")
greet("world")
}
fun greet(name: String) {
println(name)
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("main.kt", src)
require.NoError(t, err)
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
assert.GreaterOrEqual(t, len(calls), 2, "expected at least 2 call edges")
// Verify call targets contain println and greet.
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")
}
func TestKotlinExtractor_TypeEnv_ExplicitType(t *testing.T) {
src := []byte(`class UserService {
fun save() {}
}
fun main() {
val svc: UserService = UserService()
svc.save()
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("app.kt", src)
require.NoError(t, err)
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
var saveCall *graph.Edge
for _, c := range calls {
if strings.HasSuffix(c.To, "save") {
saveCall = c
break
}
}
require.NotNil(t, saveCall, "expected a call edge to save")
require.NotNil(t, saveCall.Meta, "expected Meta on save call edge")
assert.Equal(t, "UserService", saveCall.Meta["receiver_type"])
}
func TestKotlinExtractor_TypeEnv_Constructor(t *testing.T) {
src := []byte(`class Client {
fun connect() {}
}
fun main() {
val client = Client()
client.connect()
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("app.kt", src)
require.NoError(t, err)
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
var connectCall *graph.Edge
for _, c := range calls {
if strings.HasSuffix(c.To, "connect") {
connectCall = c
break
}
}
require.NotNil(t, connectCall)
require.NotNil(t, connectCall.Meta)
assert.Equal(t, "Client", connectCall.Meta["receiver_type"])
}
func TestKotlinExtractor_TypeEnv_Unknown(t *testing.T) {
src := []byte(`fun getService(): Any = TODO()
fun main() {
val svc = getService()
svc.process()
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("app.kt", src)
require.NoError(t, err)
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
var processCall *graph.Edge
for _, c := range calls {
if strings.HasSuffix(c.To, "process") {
processCall = c
break
}
}
require.NotNil(t, processCall)
assert.Nil(t, processCall.Meta, "unknown type should not produce Meta")
}
func TestKotlinExtractor_TypeEnv_Chain(t *testing.T) {
src := []byte(`class Order {
val id: Int = 0
}
class UserService {
fun getOrder(): Order {
return Order()
}
}
fun main() {
val svc = UserService()
svc.getOrder().toString()
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("app.kt", src)
require.NoError(t, err)
// Verify return_type is set on getOrder method.
var getOrderNode *graph.Node
for _, n := range result.Nodes {
if n.Name == "getOrder" {
getOrderNode = n
break
}
}
require.NotNil(t, getOrderNode, "expected a node for getOrder")
assert.Equal(t, "Order", getOrderNode.Meta["return_type"])
// Verify chain resolution: svc.getOrder() should resolve to Order.
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
var toStringCall *graph.Edge
for _, c := range calls {
if strings.HasSuffix(c.To, "toString") {
toStringCall = c
break
}
}
require.NotNil(t, toStringCall, "expected a call edge to toString")
require.NotNil(t, toStringCall.Meta, "expected Meta on toString call edge")
assert.Equal(t, "Order", toStringCall.Meta["receiver_type"])
}
func TestKotlinExtractor_DocAndVisibility(t *testing.T) {
src := []byte(`package x
/**
* Greeter does the thing.
*/
class Greeter {
/** Says hi. */
fun hello() {}
private fun secret() {}
}
internal fun helper() {}
`)
e := NewKotlinExtractor()
result, err := e.Extract("Greeter.kt", src)
require.NoError(t, err)
byID := map[string]*graph.Node{}
for _, n := range result.Nodes {
byID[n.ID] = n
}
greeter := byID["Greeter.kt::Greeter"]
require.NotNil(t, greeter)
if greeter.Meta["visibility"] != "public" {
t.Fatalf("Greeter.vis = %q", greeter.Meta["visibility"])
}
if greeter.Meta["doc"] != "Greeter does the thing." {
t.Fatalf("Greeter.doc = %q", greeter.Meta["doc"])
}
hello := byID["Greeter.kt::Greeter.hello"]
require.NotNil(t, hello)
if hello.Meta["visibility"] != "public" {
t.Fatalf("hello.vis = %q", hello.Meta["visibility"])
}
if hello.Meta["doc"] != "Says hi." {
t.Fatalf("hello.doc = %q", hello.Meta["doc"])
}
secret := byID["Greeter.kt::Greeter.secret"]
require.NotNil(t, secret)
if secret.Meta["visibility"] != "private" {
t.Fatalf("secret.vis = %q", secret.Meta["visibility"])
}
helper := byID["Greeter.kt::helper"]
require.NotNil(t, helper)
if helper.Meta["visibility"] != "internal" {
t.Fatalf("helper.vis = %q", helper.Meta["visibility"])
}
}
// kotlinCallEdgeTo returns the first EdgeCalls whose target ends in the
// given method name. Mirrors the lookup the type-env tests use.
func kotlinCallEdgeTo(result *parser.ExtractionResult, method string) *graph.Edge {
for _, c := range edgesOfKind(result.Edges, graph.EdgeCalls) {
if strings.HasSuffix(c.To, "."+method) {
return c
}
}
return nil
}
func TestKotlinExtractor_CompanionObject_AnonymousStaticDispatch(t *testing.T) {
src := []byte(`class Foo {
companion object {
fun create(): Foo {
return Foo()
}
}
}
fun main() {
Foo.create()
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("Foo.kt", src)
require.NoError(t, err)
byID := map[string]*graph.Node{}
for _, n := range result.Nodes {
byID[n.ID] = n
}
// The companion function is attributed to the enclosing class Foo.
create := byID["Foo.kt::Foo.create"]
require.NotNil(t, create, "companion create should be a member of Foo")
assert.Equal(t, graph.KindMethod, create.Kind)
assert.Equal(t, "Foo", create.Meta["receiver"])
assert.Equal(t, true, create.Meta["static"])
assert.Equal(t, true, create.Meta["companion"])
// EdgeMemberOf should point the method at Foo.
var memberOf bool
for _, ed := range edgesOfKind(result.Edges, graph.EdgeMemberOf) {
if ed.From == "Foo.kt::Foo.create" && ed.To == "Foo.kt::Foo" {
memberOf = true
}
}
assert.True(t, memberOf, "expected create -> Foo member_of edge")
// The Foo.create() call carries receiver_type=Foo so the resolver
// can land it on the companion's create method.
callEdge := kotlinCallEdgeTo(result, "create")
require.NotNil(t, callEdge, "expected a call edge to create")
require.NotNil(t, callEdge.Meta, "expected receiver_type meta on Foo.create() call")
assert.Equal(t, "Foo", callEdge.Meta["receiver_type"])
}
func TestKotlinExtractor_CompanionObject_NamedStaticDispatch(t *testing.T) {
src := []byte(`class Bar {
companion object Factory {
@JvmStatic
fun thing(): Bar = Bar()
}
}
fun main() {
Bar.thing()
Bar.Factory.thing()
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("Bar.kt", src)
require.NoError(t, err)
byID := map[string]*graph.Node{}
for _, n := range result.Nodes {
byID[n.ID] = n
}
// Both the type-receiver method (Bar.thing) and the named-companion
// alias (Bar.Factory.thing) exist.
barThing := byID["Bar.kt::Bar.thing"]
require.NotNil(t, barThing, "expected Bar.thing member")
assert.Equal(t, "Bar", barThing.Meta["receiver"])
assert.Equal(t, "Factory", barThing.Meta["companion_name"])
aliasThing := byID["Bar.kt::Bar.Factory.thing"]
require.NotNil(t, aliasThing, "expected Bar.Factory.thing alias member")
assert.Equal(t, "Bar.Factory", aliasThing.Meta["receiver"])
assert.Equal(t, true, aliasThing.Meta["companion_alias"])
// Bar.thing() resolves via receiver_type=Bar; Bar.Factory.thing()
// resolves via receiver_type=Bar.Factory.
var sawBar, sawFactory bool
for _, c := range edgesOfKind(result.Edges, graph.EdgeCalls) {
if !strings.HasSuffix(c.To, ".thing") || c.Meta == nil {
continue
}
switch c.Meta["receiver_type"] {
case "Bar":
sawBar = true
case "Bar.Factory":
sawFactory = true
}
}
assert.True(t, sawBar, "expected Bar.thing() call with receiver_type=Bar")
assert.True(t, sawFactory, "expected Bar.Factory.thing() call with receiver_type=Bar.Factory")
}
func TestKotlinExtractor_CompanionObject_ConstProperty(t *testing.T) {
src := []byte(`class Config {
companion object {
const val NAME = "config"
val version = "1.0"
}
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("Config.kt", src)
require.NoError(t, err)
byID := map[string]*graph.Node{}
for _, n := range result.Nodes {
byID[n.ID] = n
}
name := byID["Config.kt::Config.NAME"]
require.NotNil(t, name, "companion const should be a member of Config")
assert.Equal(t, graph.KindConstant, name.Kind)
assert.Equal(t, "Config", name.Meta["receiver"])
assert.Equal(t, true, name.Meta["static"])
version := byID["Config.kt::Config.version"]
require.NotNil(t, version, "companion val should be a member of Config")
assert.Equal(t, graph.KindField, version.Kind)
assert.Equal(t, "Config", version.Meta["receiver"])
// Companion props must not be emitted as top-level variables.
for _, n := range nodesOfKind(result.Nodes, graph.KindVariable) {
assert.NotEqual(t, "NAME", n.Name)
assert.NotEqual(t, "version", n.Name)
}
}
func TestKotlinExtractor_LambdaParam_NotMisresolved(t *testing.T) {
// `item` is also the name of a top-level value of a known type;
// inside the lambda it is a different, locally-bound parameter. The
// lambda use of item.toLong() must NOT pick up the outer item's type.
src := []byte(`class Widget {
fun render() {}
}
fun process(list: List<Int>) {
val item = Widget()
list.map { item -> item.toLong() }
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("app.kt", src)
require.NoError(t, err)
toLong := kotlinCallEdgeTo(result, "toLong")
require.NotNil(t, toLong, "expected a call edge to toLong")
// The lambda parameter shadows the outer `item`, so no outer
// receiver_type (Widget) may be attached.
if toLong.Meta != nil {
assert.Nil(t, toLong.Meta["receiver_type"],
"lambda-param receiver must not be resolved against the outer type env")
}
}
func TestKotlinExtractor_LambdaImplicitIt_NotMisresolved(t *testing.T) {
// A top-level `it` of a known type exists; the implicit lambda `it`
// must shadow it inside `forEach { it.compute() }`.
src := []byte(`class Engine {
fun compute() {}
}
fun run(list: List<Int>) {
val it = Engine()
list.forEach { it.compute() }
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("run.kt", src)
require.NoError(t, err)
compute := kotlinCallEdgeTo(result, "compute")
require.NotNil(t, compute, "expected a call edge to compute")
if compute.Meta != nil {
assert.Nil(t, compute.Meta["receiver_type"],
"implicit lambda `it` must not be resolved against the outer type env")
}
}
// TestKotlinConstClassificationAndPackageScope is part of the C9 set: a
// top-level `const val` classifies as a constant and package scope is stamped.
func TestKotlinConstClassificationAndPackageScope(t *testing.T) {
src := []byte("package com.app\nconst val MAX = 10\nval other = 2\n")
res, err := NewKotlinExtractor().Extract("a.kt", src)
require.NoError(t, err)
byName := map[string]*graph.Node{}
for _, n := range res.Nodes {
byName[n.Name] = n
}
require.NotNil(t, byName["MAX"])
assert.Equal(t, graph.KindConstant, byName["MAX"].Kind, "const val → constant")
assert.Equal(t, "com.app", byName["MAX"].Meta["scope_pkg"])
require.NotNil(t, byName["other"])
assert.Equal(t, "com.app", byName["other"].Meta["scope_pkg"])
}
func TestKotlinExtractor_DataClassSynthesizesComponentAndCopy(t *testing.T) {
src := []byte(`data class P(val a: A, val b: B)
`)
e := NewKotlinExtractor()
result, err := e.Extract("P.kt", src)
require.NoError(t, err)
byName := map[string]*graph.Node{}
for _, n := range nodesOfKind(result.Nodes, graph.KindMethod) {
byName[n.Name] = n
}
for _, tc := range []struct{ name, ret string }{
{"component1", "A"},
{"component2", "B"},
{"copy", "P"},
} {
n := byName[tc.name]
require.NotNilf(t, n, "%s not synthesized", tc.name)
assert.Equal(t, "data_class", n.Meta["synthetic"])
assert.Equal(t, true, n.Meta["generated"])
assert.Equal(t, tc.ret, n.Meta["return_type"])
found := false
for _, ed := range edgesOfKind(result.Edges, graph.EdgeMemberOf) {
if ed.From == n.ID && ed.To == "P.kt::P" {
found = true
}
}
assert.Truef(t, found, "%s missing member_of edge to P", tc.name)
}
}
func TestKotlinExtractor_NonDataClassNoSynthesis(t *testing.T) {
src := []byte(`class Q(val a: A)
`)
e := NewKotlinExtractor()
result, err := e.Extract("Q.kt", src)
require.NoError(t, err)
for _, n := range result.Nodes {
if n.Kind == graph.KindMethod {
assert.NotEqual(t, "data_class", n.Meta["synthetic"], "non-data class must not synthesize members")
}
assert.NotEqual(t, "Q.kt::Q.component1", n.ID, "non-data class must not synthesize component1")
}
}
func TestKotlinExtractor_DataClassUserCopyNotDuplicated(t *testing.T) {
src := []byte(`data class R(val a: A) {
fun copy(): R = this
}
`)
e := NewKotlinExtractor()
result, err := e.Extract("R.kt", src)
require.NoError(t, err)
copies := 0
hasComp1 := false
for _, n := range nodesOfKind(result.Nodes, graph.KindMethod) {
switch n.Name {
case "copy":
copies++
assert.NotEqual(t, "data_class", n.Meta["synthetic"], "user copy must not be marked synthetic")
case "component1":
if n.Meta["synthetic"] == "data_class" {
hasComp1 = true
}
}
}
assert.Equal(t, 1, copies, "user-declared copy must not be duplicated by synthesis")
assert.True(t, hasComp1, "component1 should still be synthesized when only copy is user-declared")
}