Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions go/internal/detector/jvm/kotlin/ktor_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ import (
// KtorRouteDetector mirrors Java KtorRouteDetector regex tier. Detects
// `routing { get("/p") { } }` blocks, `route("/api") {` prefixes,
// `authenticate("...") {` guards, and `install(...)` features.
//
// REQUIRES a Ktor-specific discriminator import (`io.ktor`) to avoid false
// positives on plain Kotlin code that uses similar DSL idioms (e.g. custom
// routing DSLs, test fixtures, or coroutine builders named `routing`/`get`).
type KtorRouteDetector struct{}

func NewKtorRouteDetector() *KtorRouteDetector { return &KtorRouteDetector{} }
Expand Down Expand Up @@ -71,6 +75,14 @@ func (d KtorRouteDetector) Detect(ctx *detector.Context) *detector.Result {
if text == "" {
return detector.EmptyResult()
}

// Discriminator: require an io.ktor import to avoid false positives on
// plain Kotlin code that uses DSL patterns (routing {}, get("...") {})
// without any Ktor dependency.
if !strings.Contains(text, "io.ktor") {
return detector.EmptyResult()
}

fp := ctx.FilePath
var nodes []*model.CodeNode

Expand Down
44 changes: 44 additions & 0 deletions go/internal/detector/jvm/kotlin/ktor_routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,50 @@ func TestKtorRoutesNegative(t *testing.T) {
}
}

// TestKtorRoutesNoFireOnPlainDSL verifies the import discriminator prevents false
// positives when plain Kotlin code uses DSL patterns that visually resemble Ktor
// (routing {}, get("/path") {}, install(...)) but has no io.ktor import.
func TestKtorRoutesNoFireOnPlainDSL(t *testing.T) {
d := NewKtorRouteDetector()
plainWithDSL := `package com.example

fun runTest() {
routing { println("not ktor") }
get("/fake") { doSomething() }
post("/fake") { doSomething() }
install(Something)
}
`
ctx := &detector.Context{FilePath: "src/PlainUtils.kt", Language: "kotlin", Content: plainWithDSL}
r := d.Detect(ctx)
if len(r.Nodes) != 0 {
t.Fatalf("expected 0 nodes on plain Kotlin DSL without io.ktor import, got %d nodes", len(r.Nodes))
}
}

// TestKtorRoutesNoFireOnFixturePlainUtils verifies zero framework emissions on
// the PlainUtils.kt fixture (stdlib only, no framework imports).
func TestKtorRoutesNoFireOnFixturePlainUtils(t *testing.T) {
d := NewKtorRouteDetector()
plainUtils := `package com.example.utils

fun add(a: Int, b: Int): Int = a + b

fun greet(name: String): String = "Hello, $name!"

class Counter(initial: Int) {
private var count = initial
fun increment() { count++ }
fun value(): Int = count
}
`
ctx := &detector.Context{FilePath: "src/PlainUtils.kt", Language: "kotlin", Content: plainUtils}
r := d.Detect(ctx)
if len(r.Nodes) != 0 {
t.Fatalf("expected 0 framework nodes on PlainUtils.kt, got %d", len(r.Nodes))
}
}

func TestKtorRoutesDeterminism(t *testing.T) {
d := NewKtorRouteDetector()
ctx := &detector.Context{FilePath: "src/Routes.kt", Language: "kotlin", Content: ktorRoutesSample}
Expand Down
38 changes: 38 additions & 0 deletions go/internal/detector/jvm/scala/scala_structures_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,44 @@ func TestScalaStructuresNegative(t *testing.T) {
}
}

// TestScalaStructuresNoFrameworkEmissions verifies the structures detector emits
// ONLY structural nodes (class/interface/method) on plain Scala files — no
// framework-flavored (endpoint, middleware, guard) nodes regardless of content.
func TestScalaStructuresNoFrameworkEmissions(t *testing.T) {
d := NewScalaStructuresDetector()
plainUtils := `package com.example.utils

object PlainUtils {
def add(a: Int, b: Int): Int = a + b
def greet(name: String): String = s"Hello, $name!"
}

class Counter(initial: Int) {
private var count = initial
def increment(): Unit = { count += 1 }
def value: Int = count
}
`
ctx := &detector.Context{FilePath: "PlainUtils.scala", Language: "scala", Content: plainUtils}
r := d.Detect(ctx)
for _, n := range r.Nodes {
switch n.Kind {
case model.NodeClass, model.NodeInterface, model.NodeMethod:
// expected structural nodes — OK
default:
t.Errorf("unexpected framework node kind %q (id=%q) on plain Scala file", n.Kind, n.ID)
}
}
for _, e := range r.Edges {
switch e.Kind {
case model.EdgeImports, model.EdgeExtends, model.EdgeImplements:
// expected structural edges — OK
default:
t.Errorf("unexpected framework edge kind %q on plain Scala file", e.Kind)
}
}
}

func TestScalaStructuresDeterminism(t *testing.T) {
d := NewScalaStructuresDetector()
ctx := &detector.Context{FilePath: "src/A.scala", Language: "scala", Content: scalaStructuresSample}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.exposed

import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll
import org.jetbrains.exposed.sql.transactions.transaction

object Users : Table("users") {
val id = integer("id").autoIncrement()
val name = varchar("name", 128)
override val primaryKey = PrimaryKey(id)
}

fun insertUser(name: String) = transaction {
Users.insert { it[Users.name] = name }
}

fun listUsers(): List<String> = transaction {
Users.selectAll().map { it[Users.name] }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.example.ktor

import io.ktor.server.application.*
import io.ktor.server.routing.*
import io.ktor.server.response.*
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty

fun Application.module() {
routing {
route("/api") {
get("/health") {
call.respondText("OK")
}
post("/echo") {
call.respondText("echo")
}
}
}
}

fun main() {
embeddedServer(Netty, port = 8080, module = Application::module).start(wait = true)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.parser

fun parseInts(s: String): List<Int> =
s.split(",").mapNotNull { it.trim().toIntOrNull() }

fun tokenize(s: String): List<String> =
s.split("\\s+".toRegex()).filter { it.isNotEmpty() }

data class Token(val kind: String, val value: String)

interface Parseable {
fun parse(input: String): List<Token>
}

class SimpleParser : Parseable {
override fun parse(input: String): List<Token> =
tokenize(input).map { Token("word", it) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.utils

fun add(a: Int, b: Int): Int = a + b

fun greet(name: String): String = "Hello, $name!"

fun <A> reverseList(xs: List<A>): List<A> = xs.reversed()

class Counter(initial: Int) {
private var count = initial
fun increment() { count++ }
fun value(): Int = count
}

data class Pair<A, B>(val first: A, val second: B)
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.spring

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController

@SpringBootApplication
class SpringBootApp

fun main(args: Array<String>) {
runApplication<SpringBootApp>(*args)
}

@RestController
class GreetController {
@GetMapping("/hello")
fun hello(): String = "Hello, Spring!"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.akka

import akka.actor.{Actor, ActorSystem, Props}
import akka.actor.ActorRef

class GreetActor extends Actor {
def receive: Receive = {
case msg: String => println(s"Got: $msg")
}
}

object AkkaService {
def main(args: Array[String]): Unit = {
val system = ActorSystem("demo")
val ref: ActorRef = system.actorOf(Props[GreetActor](), "greeter")
ref ! "hello"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.cats

import cats.effect.{IO, IOApp}
import cats.effect.ExitCode

object CatsEffectApp extends IOApp {
def run(args: List[String]): IO[ExitCode] =
IO.println("hello cats").as(ExitCode.Success)

def fetchData(url: String): IO[String] =
IO.delay(s"response from $url")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.parser

object PlainParser {
def parseInts(s: String): List[Int] =
s.split(",").toList.flatMap { tok =>
tok.trim.toIntOption.toList
}

def tokenize(s: String): List[String] =
s.split("\\s+").toList.filter(_.nonEmpty)
}

case class Token(kind: String, value: String)

trait Parseable {
def parse(input: String): List[Token]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.example.utils

object PlainUtils {
def add(a: Int, b: Int): Int = a + b

def greet(name: String): String = s"Hello, $name!"

def reverseList[A](xs: List[A]): List[A] = xs.reverse
}

class Counter(initial: Int) {
private var count = initial
def increment(): Unit = { count += 1 }
def value: Int = count
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.slick

import slick.jdbc.H2Profile.api._
import slick.lifted.TableQuery

class UsersTable(tag: Tag) extends Table[(Int, String)](tag, "USERS") {
def id = column[Int]("ID", O.PrimaryKey)
def name = column[String]("NAME")
def * = (id, name)
}

object SlickRepo {
val users = TableQuery[UsersTable]

def insertUser(id: Int, name: String) =
users += (id, name)
}
Loading