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
729 lines
20 KiB
Go
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")
|
|
}
|