package main import ( "os" "path/filepath" "strings" "testing" "gotlin/internal/lang" ) func TestPackageWideDiagnosticsAndDefinition(t *testing.T) { root := t.TempDir() if err := os.WriteFile(filepath.Join(root, "go.mod"), []byte("module example\n\ngo 1.25\n"), 0o644); err != nil { t.Fatal(err) } if err := os.Mkdir(filepath.Join(root, "model"), 0o755); err != nil { t.Fatal(err) } modelPath := filepath.Join(root, "model", "command.gt") modelText := "package main\n\ndata class CreateAccountCommand(var name: String)\n" if err := os.WriteFile(modelPath, []byte(modelText), 0o644); err != nil { t.Fatal(err) } mainPath := filepath.Join(root, "main.gt") mainText := "package main\n\nfun main() {\n val command = CreateAccountCommand(\"demo\")\n println(command)\n}\n" if err := os.WriteFile(mainPath, []byte(mainText), 0o644); err != nil { t.Fatal(err) } uri := fileURI(mainPath) s := server{docs: map[string]documentState{}} state := s.buildDocumentState(uri, mainText) for _, d := range state.diagnostics { if strings.Contains(d.Message, "undefined identifier CreateAccountCommand") { t.Fatalf("unexpected cross-file diagnostic: %+v", d) } } s.docs[uri] = state character := strings.Index(strings.Split(mainText, "\n")[3], "CreateAccountCommand") result, ok := s.definition(uri, position{Line: 3, Character: character + 2}).([]location) if !ok || len(result) != 1 { t.Fatalf("expected cross-file definition, got %#v", s.definition(uri, position{Line: 3, Character: character + 2})) } if result[0].URI != fileURI(modelPath) { t.Fatalf("definition URI = %s, want %s", result[0].URI, fileURI(modelPath)) } } func TestPackageIndexStopsAtNearestGoModule(t *testing.T) { root := t.TempDir() first := filepath.Join(root, "first") second := filepath.Join(root, "second") for _, dir := range []string{first, second} { if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte("module example\n\ngo 1.25\n"), 0o644); err != nil { t.Fatal(err) } } if err := os.WriteFile(filepath.Join(second, "other.gt"), []byte("package main\ndata class OtherRootType(var id: String)\n"), 0o644); err != nil { t.Fatal(err) } mainPath := filepath.Join(first, "main.gt") mainText := "package main\nfun main() { OtherRootType(\"x\") }\n" if err := os.WriteFile(mainPath, []byte(mainText), 0o644); err != nil { t.Fatal(err) } s := server{docs: map[string]documentState{}} state := s.buildDocumentState(fileURI(mainPath), mainText) found := false for _, d := range state.diagnostics { if strings.Contains(d.Message, "undefined identifier OtherRootType") { found = true } } if !found { t.Fatalf("expected module-isolated undefined diagnostic, got %+v", state.diagnostics) } } func TestCurrentBuiltinsDoNotProduceUndefinedDiagnostics(t *testing.T) { state := buildDocumentState(`package demo fun main() { val bytes = ByteSlice("hello") var values: MutableList = mutableListOf() values = append(values, string(bytes)) println(len(values)) }`) for _, diagnostic := range state.diagnostics { if strings.Contains(diagnostic.Message, "undefined identifier") { t.Fatalf("unexpected builtin diagnostic: %+v", diagnostic) } } for _, name := range []string{"ByteSlice", "append", "keys", "goAssert", "len", "sql", "set", "now"} { if !isBuiltin(name) { t.Fatalf("%s is not registered as builtin", name) } } } func TestResolveStdlibTarget(t *testing.T) { text := strings.TrimSpace(` package examples.http import fmt import net.http fun main() { fmt.Println("hello") http.HandleFunc("/") { w, r -> helloHandler(w, r) } } `) program, err := lang.Parse(text) if err != nil { t.Fatalf("parse failed: %v", err) } state := documentState{ text: text, program: program, } printPos := position{Line: 6, Character: 9} target, ok := resolveStdlibTarget(state, printPos) if !ok { t.Fatal("expected fmt.Println to resolve as stdlib target") } if target.PackagePath != "fmt" || target.SymbolName != "Println" { t.Fatalf("unexpected target: %+v", target) } httpPos := position{Line: 7, Character: 6} target, ok = resolveStdlibTarget(state, httpPos) if !ok { t.Fatal("expected import alias to resolve as stdlib package") } if target.PackagePath != "net/http" || target.SymbolName != "" { t.Fatalf("unexpected package target: %+v", target) } } func TestFindStdlibSymbol(t *testing.T) { sym, ok := findStdlibSymbol(stdlibTarget{ PackagePath: "fmt", SymbolName: "Println", }) if !ok { t.Fatal("expected fmt.Println to resolve in local Go stdlib") } if !strings.HasSuffix(sym.FileName, "/src/fmt/print.go") { t.Fatalf("unexpected file: %s", sym.FileName) } if !strings.Contains(sym.Decl, "func Println") { t.Fatalf("unexpected decl: %s", sym.Decl) } } func TestFindStdlibSymbolMethod(t *testing.T) { sym, ok := findStdlibSymbol(stdlibTarget{ PackagePath: "bytes", SymbolName: "WriteString", }) if !ok { t.Fatal("expected bytes.(*Buffer).WriteString to resolve in local Go sources") } if !strings.Contains(sym.Decl, "WriteString") { t.Fatalf("unexpected decl: %s", sym.Decl) } } func TestResolveStdlibTargetThirdPartyImportPathMapping(t *testing.T) { text := strings.TrimSpace(` package demo import github.com.uptrace.bun fun main() { bun.NewDB(nil, nil) } `) program, err := lang.Parse(text) if err != nil { t.Fatalf("parse failed: %v", err) } state := documentState{ text: text, program: program, } pos := position{Line: 5, Character: 8} target, ok := resolveStdlibTarget(state, pos) if !ok { t.Fatal("expected bun.NewDB to resolve as package target") } if target.PackagePath != "github.com/uptrace/bun" || target.SymbolName != "NewDB" { t.Fatalf("unexpected target: %+v", target) } } func TestResolveStdlibTargetFieldSelectorImportMapping(t *testing.T) { text := strings.TrimSpace(` package demo import github.com.uptrace.bun class EpicControllerImpl(val db: *bun.DB) { fun bunHealth() { db.NewSelect() } } `) program, err := lang.Parse(text) if err != nil { t.Fatalf("parse failed: %v", err) } state := documentState{ text: text, program: program, } pos := position{Line: 6, Character: 12} target, ok := resolveStdlibTarget(state, pos) if !ok { t.Fatal("expected db.NewSelect to resolve as imported package symbol") } if target.PackagePath != "github.com/uptrace/bun" || target.SymbolName != "NewSelect" { t.Fatalf("unexpected target: %+v", target) } } func TestResolveStdlibTargetNestedFieldSelectorImportMapping(t *testing.T) { text := strings.TrimSpace(` package demo import context import github.com.uptrace.bun class EpicControllerImpl(val db: *bun.DB) { fun bunHealth() { val ctx = context.Background() db.NewSelect().ColumnExpr("1").Scan(ctx) } } `) program, err := lang.Parse(text) if err != nil { t.Fatalf("parse failed: %v", err) } state := documentState{ text: text, program: program, } pos := position{Line: 8, Character: 40} target, ok := resolveStdlibTarget(state, pos) if !ok { t.Fatal("expected nested db selector to resolve as imported package symbol") } if target.PackagePath != "github.com/uptrace/bun" || target.SymbolName != "Scan" { t.Fatalf("unexpected target: %+v", target) } } func TestBuildDocumentStateIndexesClassesAndInterfaces(t *testing.T) { text := strings.TrimSpace(` package demo interface Greeter { fun greet(name: String): String } class ConsoleGreeter(val prefix: String) { fun greet(name: String): String { return prefix + name } } `) state := buildDocumentState(text) if state.program == nil { t.Fatal("expected parsed program") } wantKinds := map[string]int{ "Greeter": symbolKindInterface, "ConsoleGreeter": symbolKindClass, "greet": symbolKindMethod, } for name, kind := range wantKinds { found := false for _, sym := range state.symbols { if sym.Name == name && sym.Kind == kind { found = true break } } if !found { t.Fatalf("missing symbol %q of kind %d", name, kind) } } } func TestBuildDocumentStateClassSemantics(t *testing.T) { text := strings.TrimSpace(` package demo interface Greeter { fun greet(name: String): String } class ConsoleGreeter(val prefix: String) { fun greet(name: String): String { return prefix + name } } fun main() { val greeter: Greeter = ConsoleGreeter("hello, ") println(greeter.greet("gotlin")) } `) state := buildDocumentState(text) if len(state.diagnostics) != 0 { t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) } } func TestBuildDocumentStateHTTPServerClassSyntax(t *testing.T) { text := strings.TrimSpace(` package examples.http import fmt import net.http interface EpicController { fun hello(w: http.ResponseWriter, r: *http.Request) } class EpicControllerImpl: EpicController { override fun hello(w: http.ResponseWriter, r: *http.Request) { fmt.Fprintln(w, "hello from gotlin") } } `) state := buildDocumentState(text) if state.program == nil { t.Fatal("expected parsed program") } if len(state.diagnostics) != 0 { t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) } foundClass := false foundMethod := false for _, sym := range state.symbols { if sym.Name == "EpicControllerImpl" && sym.Kind == symbolKindClass { foundClass = true if sym.Detail != "class EpicControllerImpl: EpicController" { t.Fatalf("unexpected class detail: %q", sym.Detail) } } if sym.Name == "hello" && sym.Kind == symbolKindMethod && strings.Contains(sym.Detail, "EpicControllerImpl.fun hello") { foundMethod = true } } if !foundClass { t.Fatal("expected EpicControllerImpl class symbol") } if !foundMethod { t.Fatal("expected EpicControllerImpl.hello method symbol") } } func TestBuildDocumentStateIndexesClassFieldSymbols(t *testing.T) { text := strings.TrimSpace(` package demo import github.com.uptrace.bun class EpicControllerImpl(val db: *bun.DB) { fun hello() { println(db) } } `) state := buildDocumentState(text) if state.program == nil { t.Fatal("expected parsed program") } foundField := false for _, sym := range state.symbols { if sym.Name == "db" && sym.Kind == symbolKindField { foundField = true if !strings.Contains(sym.Detail, "EpicControllerImpl.val db: *bun.DB") { t.Fatalf("unexpected field detail: %q", sym.Detail) } break } } if !foundField { t.Fatal("expected db field symbol") } } func TestBuildDocumentStateIndexesLocalVariableSymbols(t *testing.T) { text := strings.TrimSpace(` package demo fun main() { val ctx = "x" var total = 1 println(ctx) println(total) } `) state := buildDocumentState(text) if state.program == nil { t.Fatal("expected parsed program") } foundCtx := false foundTotal := false for _, sym := range state.symbols { if sym.Name == "ctx" && sym.Kind == symbolKindVariable { foundCtx = true if sym.Detail != "val ctx: String" { t.Fatalf("unexpected ctx detail: %q", sym.Detail) } } if sym.Name == "total" && sym.Kind == symbolKindVariable { foundTotal = true if sym.Detail != "var total: Int" { t.Fatalf("unexpected total detail: %q", sym.Detail) } } } if !foundCtx || !foundTotal { t.Fatalf("expected local variable symbols, got %+v", state.symbols) } } func TestBuildDocumentStateInfersHttpServerVariableTypes(t *testing.T) { text := strings.TrimSpace(` package demo import context import github.com.uptrace.bun class C(val db: *bun.DB) { fun run() { val ctx = context.Background() val total = db.NewSelect().ColumnExpr("1").Count(ctx) println(total) } } `) state := buildDocumentState(text) if state.program == nil { t.Fatal("expected parsed program") } seenCtx := false seenTotal := false for _, sym := range state.symbols { if sym.Kind != symbolKindVariable { continue } if sym.Name == "ctx" { seenCtx = true if sym.Detail != "val ctx: context.Context" { t.Fatalf("unexpected ctx detail: %q", sym.Detail) } } if sym.Name == "total" { seenTotal = true if sym.Detail != "val total: Int" { t.Fatalf("unexpected total detail: %q", sym.Detail) } } } if !seenCtx || !seenTotal { t.Fatalf("missing inferred variable symbols; got %+v", state.symbols) } } func TestBuildDocumentStateVariableTypeMatchingByName(t *testing.T) { text := strings.TrimSpace(` package demo class BunUser(val Name: String) class EpicControllerImpl fun main() { val epicController = EpicControllerImpl() val user = BunUser("user-from-gotlin") println(epicController) println(user) } `) state := buildDocumentState(text) if state.program == nil { t.Fatal("expected parsed program") } var userDetail string var controllerDetail string for _, sym := range state.symbols { if sym.Kind != symbolKindVariable { continue } if sym.Name == "user" { userDetail = sym.Detail } if sym.Name == "epicController" { controllerDetail = sym.Detail } } if userDetail != "val user: BunUser" { t.Fatalf("unexpected user detail: %q", userDetail) } if controllerDetail != "val epicController: EpicControllerImpl" { t.Fatalf("unexpected epicController detail: %q", controllerDetail) } } func TestBuildDocumentStateMultiAssignAndNullSemantics(t *testing.T) { text := strings.TrimSpace(` package demo import database.sql fun main() { val db, err = sql.Open("postgres", "postgres://localhost/postgres?sslmode=disable") if (err != null) { return } db.Close() } `) state := buildDocumentState(text) if state.program == nil { t.Fatal("expected parsed program") } if len(state.diagnostics) != 0 { t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) } } func TestBuildDocumentStateExceptionsAndRunCatchingSemantics(t *testing.T) { text := strings.TrimSpace(` package demo fun mayFail() { throw "boom" } fun main() { val result = runCatching({ mayFail() }) if (result.isSuccess()) { println("ok") } else { println(result.exceptionOrNull()) } try { mayFail() } catch (e: String) { println(e) } } `) state := buildDocumentState(text) if state.program == nil { t.Fatal("expected parsed program") } if len(state.diagnostics) != 0 { t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) } } func TestBuildDocumentStateCollectionLiteralBuiltins(t *testing.T) { text := strings.TrimSpace(` package demo fun main() { val names = listOf("alice", "bob") val test = listOf(1, 7) val ages = mapOf("alice", 30, "bob", 25) println(names) println(test) println(ages) } `) state := buildDocumentState(text) if state.program == nil { t.Fatal("expected parsed program") } if len(state.diagnostics) != 0 { t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) } var namesDetail string var testDetail string var agesDetail string for _, sym := range state.symbols { if sym.Kind != symbolKindVariable { continue } if sym.Name == "names" { namesDetail = sym.Detail } if sym.Name == "test" { testDetail = sym.Detail } if sym.Name == "ages" { agesDetail = sym.Detail } } if namesDetail != "val names: List" { t.Fatalf("unexpected names detail: %q", namesDetail) } if testDetail != "val test: List" { t.Fatalf("unexpected test detail: %q", testDetail) } if agesDetail != "val ages: Map" { t.Fatalf("unexpected ages detail: %q", agesDetail) } } func TestBuildDocumentStateCollectionLiteralBuiltinsWithTypeArgs(t *testing.T) { text := strings.TrimSpace(` package demo fun main() { val test = listOf() val labels = mapOf() println(test) println(labels) } `) state := buildDocumentState(text) if state.program == nil { t.Fatal("expected parsed program") } if len(state.diagnostics) != 0 { t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) } var testDetail string var labelsDetail string for _, sym := range state.symbols { if sym.Kind != symbolKindVariable { continue } if sym.Name == "test" { testDetail = sym.Detail } if sym.Name == "labels" { labelsDetail = sym.Detail } } if testDetail != "val test: List" { t.Fatalf("unexpected test detail: %q", testDetail) } if labelsDetail != "val labels: Map" { t.Fatalf("unexpected labels detail: %q", labelsDetail) } } func TestBuildDocumentStateGoStatementSemantics(t *testing.T) { text := strings.TrimSpace(` package demo fun runWorker(name: String) { println(name) } fun main() { runBlocking { launch { runWorker("alice") } } } `) state := buildDocumentState(text) if state.program == nil { t.Fatal("expected parsed program") } if len(state.diagnostics) != 0 { t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) } } func TestBuildDocumentStateChannelSemantics(t *testing.T) { text := strings.TrimSpace(` package demo fun writer(ch: Channel) { ch.send(7) } fun main() { val ch = Channel(1) writer(ch) val v = ch.read() println(v) } `) state := buildDocumentState(text) if state.program == nil { t.Fatal("expected parsed program") } if len(state.diagnostics) != 0 { t.Fatalf("expected no diagnostics, got %+v", state.diagnostics) } var chDetail string var vDetail string for _, sym := range state.symbols { if sym.Kind != symbolKindVariable { continue } if sym.Name == "ch" { chDetail = sym.Detail } if sym.Name == "v" { vDetail = sym.Detail } } if chDetail != "val ch: Channel" { t.Fatalf("unexpected ch detail: %q", chDetail) } if vDetail != "val v: Int" { t.Fatalf("unexpected v detail: %q", vDetail) } } func TestBuildDocumentStateWorkerSemantics(t *testing.T) { text := strings.TrimSpace(` package demo worker Counter { var counter = 0 fun getCount(): Int { return counter } fun increment() { counter += 1 } } fun main() { val counter = Counter() counter.increment() println(counter.getCount()) } `) state := buildDocumentState(text) if state.program != nil { t.Fatal("worker syntax should no longer parse") } if len(state.diagnostics) == 0 { t.Fatal("expected removed worker syntax diagnostic") } }