From 2f4571b083c7781ebcd008b86cc8f5912f7927bf Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 28 Aug 2026 14:15:05 +0200 Subject: [PATCH 01/10] Kotlin: cover full value class extraction Add focused coverage for an abstract value base class and a concrete multi-field subclass with property overrides, inheritance, and a secondary constructor. The existing extractor represents these constructs correctly. This commit adds coverage only and requires no extractor change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../full-value-classes/test.expected | 17 ++++++++ .../library-tests/full-value-classes/test.kt | 11 ++++++ .../library-tests/full-value-classes/test.ql | 39 +++++++++++++++++++ .../full-value-classes/test.qlref | 1 + 4 files changed, 68 insertions(+) create mode 100644 java/ql/test-kotlin2/library-tests/full-value-classes/test.expected create mode 100644 java/ql/test-kotlin2/library-tests/full-value-classes/test.kt create mode 100644 java/ql/test-kotlin2/library-tests/full-value-classes/test.ql create mode 100644 java/ql/test-kotlin2/library-tests/full-value-classes/test.qlref diff --git a/java/ql/test-kotlin2/library-tests/full-value-classes/test.expected b/java/ql/test-kotlin2/library-tests/full-value-classes/test.expected new file mode 100644 index 000000000000..5a4f96bdaee8 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/full-value-classes/test.expected @@ -0,0 +1,17 @@ +classes +| test.kt:3:1:5:1 | Base | abstract, public | +| test.kt:7:1:9:1 | PairValue | final, public | +supertypes +| test.kt:7:1:9:1 | PairValue | test.kt:3:1:5:1 | Base | +properties +| test.kt:4:14:4:27 | value | int | public | test.kt:4:14:4:27 | getValue | | +| test.kt:7:32:7:45 | value | int | public | test.kt:7:32:7:45 | getValue | value | +| test.kt:7:48:7:64 | label | String | public | test.kt:7:48:7:64 | getLabel | label | +constructors +| test.kt:3:1:5:1 | Base | Base() | +| test.kt:7:22:7:65 | PairValue | PairValue(int,java.lang.String) | +| test.kt:8:5:8:68 | PairValue | PairValue(long) | +constructorCalls +| test.kt:7:1:9:1 | super(...) | test.kt:3:1:5:1 | Base | +| test.kt:8:32:8:68 | this(...) | test.kt:7:22:7:65 | PairValue | +| test.kt:11:40:11:55 | new PairValue(...) | test.kt:8:5:8:68 | PairValue | diff --git a/java/ql/test-kotlin2/library-tests/full-value-classes/test.kt b/java/ql/test-kotlin2/library-tests/full-value-classes/test.kt new file mode 100644 index 000000000000..50f8209367a8 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/full-value-classes/test.kt @@ -0,0 +1,11 @@ +// codeql-extractor-kotlin-options: -XXLanguage:+FullValueClasses + +abstract value class Base { + abstract val value: Int +} + +value class PairValue(override val value: Int, val label: String) : Base() { + constructor(value: Long) : this(value.toInt(), value.toString()) +} + +fun makePairValue(value: Long): Base = PairValue(value) diff --git a/java/ql/test-kotlin2/library-tests/full-value-classes/test.ql b/java/ql/test-kotlin2/library-tests/full-value-classes/test.ql new file mode 100644 index 000000000000..c59845dba7e8 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/full-value-classes/test.ql @@ -0,0 +1,39 @@ +import java + +string backingField(Property p) { + if exists(p.getBackingField()) then result = p.getBackingField().toString() else result = "" +} + +query predicate classes(Class c, string classModifiers) { + c.fromSource() and + not c.isCompilerGenerated() and + c.getLocation().getStartLine() > 0 and + classModifiers = concat(string m | c.hasModifier(m) | m, ", ") +} + +query predicate supertypes(Class c, Class supertype) { + c.fromSource() and + supertype.fromSource() and + extendsReftype(c, supertype) +} + +query predicate properties( + Property p, string propertyType, string propertyModifiers, Method getter, string field +) { + p.fromSource() and + propertyType = p.getGetter().getReturnType().toString() and + propertyModifiers = concat(string m | p.hasModifier(m) | m, ", ") and + getter = p.getGetter() and + field = backingField(p) +} + +query predicate constructors(Constructor c, string signature) { + c.fromSource() and + signature = c.getSignature() +} + +query predicate constructorCalls(ConstructorCall call, Constructor target) { + call.getEnclosingCallable().fromSource() and + target = call.getConstructor() and + target.getSourceDeclaration().fromSource() +} diff --git a/java/ql/test-kotlin2/library-tests/full-value-classes/test.qlref b/java/ql/test-kotlin2/library-tests/full-value-classes/test.qlref new file mode 100644 index 000000000000..b79433a5b459 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/full-value-classes/test.qlref @@ -0,0 +1 @@ +test.ql From 06042b1fc322eb81aa0a7eb73aaf93fade2ca778 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 28 Aug 2026 14:19:17 +0200 Subject: [PATCH 02/10] Kotlin: cover name-based destructuring Add focused coverage for short-form name-based destructuring where the selected property is not the first declared field. Check the resolved getter and data flow from the selected field to the destructured local. The existing extractor selects the property by name correctly. This commit adds coverage only and requires no extractor change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../name-based-destructuring/test.expected | 4 ++++ .../name-based-destructuring/test.kt | 14 ++++++++++++ .../name-based-destructuring/test.ql | 22 +++++++++++++++++++ .../name-based-destructuring/test.qlref | 1 + 4 files changed, 41 insertions(+) create mode 100644 java/ql/test-kotlin2/library-tests/name-based-destructuring/test.expected create mode 100644 java/ql/test-kotlin2/library-tests/name-based-destructuring/test.kt create mode 100644 java/ql/test-kotlin2/library-tests/name-based-destructuring/test.ql create mode 100644 java/ql/test-kotlin2/library-tests/name-based-destructuring/test.qlref diff --git a/java/ql/test-kotlin2/library-tests/name-based-destructuring/test.expected b/java/ql/test-kotlin2/library-tests/name-based-destructuring/test.expected new file mode 100644 index 000000000000..fd9267db21f4 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/name-based-destructuring/test.expected @@ -0,0 +1,4 @@ +selectedProperty +| test.kt:10:10:10:17 | currency | test.kt:10:10:10:17 | getCurrency(...) | test.kt:3:36:3:55 | getCurrency | +#select +| test.kt:14:28:14:35 | source(...) | test.kt:11:10:11:17 | currency | diff --git a/java/ql/test-kotlin2/library-tests/name-based-destructuring/test.kt b/java/ql/test-kotlin2/library-tests/name-based-destructuring/test.kt new file mode 100644 index 000000000000..34396bc753e8 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/name-based-destructuring/test.kt @@ -0,0 +1,14 @@ +// codeql-extractor-kotlin-options: -XXLanguage:+FullValueClasses -XXLanguage:+NameBasedDestructuring -XXLanguage:+EnableNameBasedDestructuringShortForm + +value class Money(val amount: Int, val currency: String) + +fun source(): String = "" + +fun sink(value: String) {} + +fun test(money: Money) { + val (currency) = money + sink(currency) +} + +fun flow() = test(Money(0, source())) diff --git a/java/ql/test-kotlin2/library-tests/name-based-destructuring/test.ql b/java/ql/test-kotlin2/library-tests/name-based-destructuring/test.ql new file mode 100644 index 000000000000..42dbe0643e19 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/name-based-destructuring/test.ql @@ -0,0 +1,22 @@ +import java +import semmle.code.java.dataflow.TaintTracking + +query predicate selectedProperty( + LocalVariableDeclExpr variable, MethodCall initializer, Method getter +) { + variable.getVariable().hasName("currency") and + initializer = variable.getInit() and + getter = initializer.getMethod() +} + +module Config implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node n) { n.asExpr().(MethodCall).getMethod().hasName("source") } + + predicate isSink(DataFlow::Node n) { n.asExpr().(Argument).getCall().getCallee().hasName("sink") } +} + +module Flow = TaintTracking::Global; + +from DataFlow::Node source, DataFlow::Node sink +where Flow::flow(source, sink) +select source, sink diff --git a/java/ql/test-kotlin2/library-tests/name-based-destructuring/test.qlref b/java/ql/test-kotlin2/library-tests/name-based-destructuring/test.qlref new file mode 100644 index 000000000000..b79433a5b459 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/name-based-destructuring/test.qlref @@ -0,0 +1 @@ +test.ql From 3d99697d7b4d0506ad378a5d521502f74a3efc34 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 28 Aug 2026 14:24:19 +0200 Subject: [PATCH 03/10] Kotlin: expose missing context parameters Add regression coverage for context parameters on a function and an extension-property getter. Record callable parameters and implicit call arguments. Without the follow-up fix, the test reports missing context parameters, missing arguments, and database consistency errors. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../context-parameters/DB-CHECK.expected | 0 .../context-parameters/test.expected | 13 ++++++++++ .../library-tests/context-parameters/test.kt | 16 ++++++++++++ .../library-tests/context-parameters/test.ql | 25 +++++++++++++++++++ .../context-parameters/test.qlref | 1 + 5 files changed, 55 insertions(+) create mode 100644 java/ql/test-kotlin2/library-tests/context-parameters/DB-CHECK.expected create mode 100644 java/ql/test-kotlin2/library-tests/context-parameters/test.expected create mode 100644 java/ql/test-kotlin2/library-tests/context-parameters/test.kt create mode 100644 java/ql/test-kotlin2/library-tests/context-parameters/test.ql create mode 100644 java/ql/test-kotlin2/library-tests/context-parameters/test.qlref diff --git a/java/ql/test-kotlin2/library-tests/context-parameters/DB-CHECK.expected b/java/ql/test-kotlin2/library-tests/context-parameters/DB-CHECK.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/java/ql/test-kotlin2/library-tests/context-parameters/test.expected b/java/ql/test-kotlin2/library-tests/context-parameters/test.expected new file mode 100644 index 000000000000..2371a61f6f29 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/context-parameters/test.expected @@ -0,0 +1,13 @@ +parameters +| test.kt:8:1:8:45 | logged | test.kt:7:9:7:22 | logger | 0 | Logger | +| test.kt:8:1:8:45 | logged | test.kt:8:12:8:24 | value | 1 | String | +| test.kt:12:5:12:28 | getLogged | test.kt:10:9:10:22 | logger | 1 | Logger | +| test.kt:12:5:12:28 | getLogged | test.kt:11:5:11:10 | | 0 | String | +calls +| test.kt:15:5:15:21 | logged(...) | test.kt:14:43:16:1 | invoke | test.kt:8:1:8:45 | logged | test.kt:15:5:15:21 | TestKt | 2 | +| test.kt:15:5:15:28 | getLogged(...) | test.kt:14:43:16:1 | invoke | test.kt:12:5:12:28 | getLogged | test.kt:15:5:15:28 | TestKt | 2 | +arguments +| test.kt:15:5:15:21 | logged(...) | 0 | test.kt:0:0:0:0 | p0 | +| test.kt:15:5:15:21 | logged(...) | 1 | test.kt:15:12:15:20 | "message" | +| test.kt:15:5:15:28 | getLogged(...) | 0 | test.kt:15:5:15:21 | logged(...) | +| test.kt:15:5:15:28 | getLogged(...) | 1 | test.kt:0:0:0:0 | p0 | diff --git a/java/ql/test-kotlin2/library-tests/context-parameters/test.kt b/java/ql/test-kotlin2/library-tests/context-parameters/test.kt new file mode 100644 index 000000000000..fb1cb7e02d5b --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/context-parameters/test.kt @@ -0,0 +1,16 @@ +// codeql-extractor-kotlin-options: -language-version 2.4 -Xcontext-parameters + +class Logger { + fun log(value: String) = value +} + +context(logger: Logger) +fun logged(value: String) = logger.log(value) + +context(logger: Logger) +val String.logged: String + get() = logger.log(this) + +fun use(logger: Logger) = context(logger) { + logged("message").logged +} diff --git a/java/ql/test-kotlin2/library-tests/context-parameters/test.ql b/java/ql/test-kotlin2/library-tests/context-parameters/test.ql new file mode 100644 index 000000000000..46d458a5cb5c --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/context-parameters/test.ql @@ -0,0 +1,25 @@ +import java + +predicate isContextCallable(Callable c) { c.getName() = ["logged", "getLogged"] } + +query predicate parameters(Callable callable, Parameter parameter, int index, string parameterType) { + isContextCallable(callable) and + parameter = callable.getParameter(index) and + parameterType = parameter.getType().toString() +} + +query predicate calls( + MethodCall call, Callable caller, Method target, Expr qualifier, int argumentCount +) { + caller.fromSource() and + call.getEnclosingCallable() = caller and + target = call.getMethod() and + isContextCallable(target) and + qualifier = call.getQualifier() and + argumentCount = call.getNumArgument() +} + +query predicate arguments(MethodCall call, int index, Expr argument) { + isContextCallable(call.getMethod()) and + argument = call.getArgument(index) +} diff --git a/java/ql/test-kotlin2/library-tests/context-parameters/test.qlref b/java/ql/test-kotlin2/library-tests/context-parameters/test.qlref new file mode 100644 index 000000000000..b79433a5b459 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/context-parameters/test.qlref @@ -0,0 +1 @@ +test.ql From 2942d7051e04177b5870db7ad7b47ac38d9f58db Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 28 Aug 2026 14:26:53 +0200 Subject: [PATCH 04/10] Kotlin: extract context parameters and arguments Treat Kotlin 2.4 IR context parameters as callable value parameters and map member-access arguments using their parameter kinds instead of assuming that all non-regular parameters form a prefix. This restores context parameters and implicit arguments while preserving dispatch and extension receiver handling. The change is confined to the Kotlin 2.4 compatibility source set. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../kotlin/utils/versions/v_2_4_0/IrCompat.kt | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_4_0/IrCompat.kt b/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_4_0/IrCompat.kt index 2906b18c3140..cdb4e7203a4f 100644 --- a/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_4_0/IrCompat.kt +++ b/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_4_0/IrCompat.kt @@ -3,6 +3,7 @@ package com.github.codeql.utils.versions import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrParameterKind import org.jetbrains.kotlin.ir.declarations.IrValueParameter import org.jetbrains.kotlin.ir.expressions.IrAnnotation import org.jetbrains.kotlin.ir.expressions.IrConstructorCall @@ -21,33 +22,35 @@ import org.jetbrains.kotlin.ir.types.addAnnotations * have been removed. This file provides the 2.4.0 implementations. */ -// IrFunction: valueParameters -> parameters filtered to Regular kind +private fun IrParameterKind.isCodeQlValueParameter() = + this == IrParameterKind.Context || this == IrParameterKind.Regular + +// IrFunction: valueParameters -> context and regular parameters val IrFunction.codeQlValueParameters: List - get() = parameters.filter { it.kind == org.jetbrains.kotlin.ir.declarations.IrParameterKind.Regular } + get() = parameters.filter { it.kind.isCodeQlValueParameter() } // IrFunction: extensionReceiverParameter val IrFunction.codeQlExtensionReceiverParameter: IrValueParameter? get() = parameters.firstOrNull { it.kind == org.jetbrains.kotlin.ir.declarations.IrParameterKind.ExtensionReceiver } -// Helper: get the offset of value arguments in the arguments list -private fun IrMemberAccessExpression<*>.valueArgumentOffset(): Int { - val owner = symbol.owner as? IrFunction ?: return 0 - return owner.parameters.count { it.kind != org.jetbrains.kotlin.ir.declarations.IrParameterKind.Regular } +private fun IrMemberAccessExpression<*>.valueArgumentIndices(): List { + val owner = symbol.owner as? IrFunction ?: return arguments.indices.toList() + return owner.parameters.mapIndexedNotNull { index, parameter -> + index.takeIf { parameter.kind.isCodeQlValueParameter() } + } } // IrMemberAccessExpression: valueArgumentsCount -// In 2.4.0, arguments[] includes dispatch/extension receivers before regular params val IrMemberAccessExpression<*>.codeQlValueArgumentsCount: Int - get() = arguments.size - valueArgumentOffset() + get() = valueArgumentIndices().size // IrMemberAccessExpression: getValueArgument -// In 2.4.0, arguments[] includes dispatch/extension receivers before regular params -fun IrMemberAccessExpression<*>.codeQlGetValueArgument(index: Int): IrExpression? = arguments[index + valueArgumentOffset()] +fun IrMemberAccessExpression<*>.codeQlGetValueArgument(index: Int): IrExpression? = + arguments[valueArgumentIndices()[index]] // IrMemberAccessExpression: putValueArgument -// In 2.4.0, arguments[] includes dispatch/extension receivers before regular params fun IrMemberAccessExpression<*>.codeQlPutValueArgument(index: Int, value: IrExpression?) { - arguments[index + valueArgumentOffset()] = value + arguments[valueArgumentIndices()[index]] = value } // Re-add accessor for the extensionReceiver property removed in Kotlin 2.4.0. From 05357c2981f862bb4a6b98d7e04f3d7fe73cffde Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 28 Aug 2026 14:39:37 +0200 Subject: [PATCH 05/10] Kotlin: cover context parameter setters Extend the context-parameter regression test to an extension-property setter. Check the extension receiver, context argument, assigned value, and resolved setter call. The preceding extractor fix already handles this case correctly. This commit adds coverage only and requires no additional extractor change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../context-parameters/test.expected | 19 +++++++++++++------ .../library-tests/context-parameters/test.kt | 9 +++++++-- .../library-tests/context-parameters/test.ql | 2 +- 3 files changed, 21 insertions(+), 9 deletions(-) diff --git a/java/ql/test-kotlin2/library-tests/context-parameters/test.expected b/java/ql/test-kotlin2/library-tests/context-parameters/test.expected index 2371a61f6f29..789d9f2505f7 100644 --- a/java/ql/test-kotlin2/library-tests/context-parameters/test.expected +++ b/java/ql/test-kotlin2/library-tests/context-parameters/test.expected @@ -3,11 +3,18 @@ parameters | test.kt:8:1:8:45 | logged | test.kt:8:12:8:24 | value | 1 | String | | test.kt:12:5:12:28 | getLogged | test.kt:10:9:10:22 | logger | 1 | Logger | | test.kt:12:5:12:28 | getLogged | test.kt:11:5:11:10 | | 0 | String | +| test.kt:13:5:15:5 | setLogged | test.kt:10:9:10:22 | logger | 1 | Logger | +| test.kt:13:5:15:5 | setLogged | test.kt:11:5:11:10 | | 0 | String | +| test.kt:13:5:15:5 | setLogged | test.kt:13:9:13:13 | value | 2 | String | calls -| test.kt:15:5:15:21 | logged(...) | test.kt:14:43:16:1 | invoke | test.kt:8:1:8:45 | logged | test.kt:15:5:15:21 | TestKt | 2 | -| test.kt:15:5:15:28 | getLogged(...) | test.kt:14:43:16:1 | invoke | test.kt:12:5:12:28 | getLogged | test.kt:15:5:15:28 | TestKt | 2 | +| test.kt:18:17:18:33 | logged(...) | test.kt:17:43:21:1 | invoke | test.kt:8:1:8:45 | logged | test.kt:18:17:18:33 | TestKt | 2 | +| test.kt:18:17:18:40 | getLogged(...) | test.kt:17:43:21:1 | invoke | test.kt:12:5:12:28 | getLogged | test.kt:18:17:18:40 | TestKt | 2 | +| test.kt:19:5:19:27 | setLogged(...) | test.kt:17:43:21:1 | invoke | test.kt:13:5:15:5 | setLogged | test.kt:19:5:19:27 | TestKt | 3 | arguments -| test.kt:15:5:15:21 | logged(...) | 0 | test.kt:0:0:0:0 | p0 | -| test.kt:15:5:15:21 | logged(...) | 1 | test.kt:15:12:15:20 | "message" | -| test.kt:15:5:15:28 | getLogged(...) | 0 | test.kt:15:5:15:21 | logged(...) | -| test.kt:15:5:15:28 | getLogged(...) | 1 | test.kt:0:0:0:0 | p0 | +| test.kt:18:17:18:33 | logged(...) | 0 | test.kt:0:0:0:0 | p0 | +| test.kt:18:17:18:33 | logged(...) | 1 | test.kt:18:24:18:32 | "message" | +| test.kt:18:17:18:40 | getLogged(...) | 0 | test.kt:18:17:18:33 | logged(...) | +| test.kt:18:17:18:40 | getLogged(...) | 1 | test.kt:0:0:0:0 | p0 | +| test.kt:19:5:19:27 | setLogged(...) | 0 | test.kt:19:5:19:12 | "target" | +| test.kt:19:5:19:27 | setLogged(...) | 1 | test.kt:0:0:0:0 | p0 | +| test.kt:19:5:19:27 | setLogged(...) | 2 | test.kt:19:23:19:27 | value | diff --git a/java/ql/test-kotlin2/library-tests/context-parameters/test.kt b/java/ql/test-kotlin2/library-tests/context-parameters/test.kt index fb1cb7e02d5b..640ec53f3c25 100644 --- a/java/ql/test-kotlin2/library-tests/context-parameters/test.kt +++ b/java/ql/test-kotlin2/library-tests/context-parameters/test.kt @@ -8,9 +8,14 @@ context(logger: Logger) fun logged(value: String) = logger.log(value) context(logger: Logger) -val String.logged: String +var String.logged: String get() = logger.log(this) + set(value) { + logger.log(value) + } fun use(logger: Logger) = context(logger) { - logged("message").logged + val value = logged("message").logged + "target".logged = value + value } diff --git a/java/ql/test-kotlin2/library-tests/context-parameters/test.ql b/java/ql/test-kotlin2/library-tests/context-parameters/test.ql index 46d458a5cb5c..d522564ed90f 100644 --- a/java/ql/test-kotlin2/library-tests/context-parameters/test.ql +++ b/java/ql/test-kotlin2/library-tests/context-parameters/test.ql @@ -1,6 +1,6 @@ import java -predicate isContextCallable(Callable c) { c.getName() = ["logged", "getLogged"] } +predicate isContextCallable(Callable c) { c.getName() = ["logged", "getLogged", "setLogged"] } query predicate parameters(Callable callable, Parameter parameter, int index, string parameterType) { isContextCallable(callable) and From 8595a36f4c541a3fa25c0eb964417e374f89ed15 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 28 Aug 2026 14:47:24 +0200 Subject: [PATCH 06/10] Kotlin: cover collection literal extraction Add focused coverage for a collection literal resolved through a companion operator fun of. Check the call target, arguments, result type, locations, and element data flow through the resulting collection. The existing extractor handles the lowered call correctly. This commit adds coverage only and requires no extractor change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../test.expected | 7 +++++ .../collection-literal-operators/test.kt | 16 ++++++++++ .../collection-literal-operators/test.ql | 30 +++++++++++++++++++ .../collection-literal-operators/test.qlref | 1 + 4 files changed, 54 insertions(+) create mode 100644 java/ql/test-kotlin2/library-tests/collection-literal-operators/test.expected create mode 100644 java/ql/test-kotlin2/library-tests/collection-literal-operators/test.kt create mode 100644 java/ql/test-kotlin2/library-tests/collection-literal-operators/test.ql create mode 100644 java/ql/test-kotlin2/library-tests/collection-literal-operators/test.qlref diff --git a/java/ql/test-kotlin2/library-tests/collection-literal-operators/test.expected b/java/ql/test-kotlin2/library-tests/collection-literal-operators/test.expected new file mode 100644 index 000000000000..e26a10b424d6 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/collection-literal-operators/test.expected @@ -0,0 +1,7 @@ +literalCall +| test.kt:14:24:14:40 | of(...) | test.kt:5:18:5:62 | of | Words | test.kt:14:24:14:40 | Companion | 2 | +literalArguments +| test.kt:14:24:14:40 | of(...) | 0 | test.kt:14:25:14:32 | source(...) | +| test.kt:14:24:14:40 | of(...) | 1 | test.kt:14:35:14:39 | "two" | +#select +| test.kt:14:25:14:32 | source(...) | test.kt:15:10:15:24 | ...[...] | diff --git a/java/ql/test-kotlin2/library-tests/collection-literal-operators/test.kt b/java/ql/test-kotlin2/library-tests/collection-literal-operators/test.kt new file mode 100644 index 000000000000..521c49b3858e --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/collection-literal-operators/test.kt @@ -0,0 +1,16 @@ +// codeql-extractor-kotlin-options: -language-version 2.4 -XXLanguage:+CollectionLiterals + +class Words private constructor(val values: Array) { + companion object { + operator fun of(vararg values: String) = Words(values) + } +} + +fun source(): String = "" + +fun sink(value: String) {} + +fun test() { + val words: Words = [source(), "two"] + sink(words.values[0]) +} diff --git a/java/ql/test-kotlin2/library-tests/collection-literal-operators/test.ql b/java/ql/test-kotlin2/library-tests/collection-literal-operators/test.ql new file mode 100644 index 000000000000..e81cc5a9888a --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/collection-literal-operators/test.ql @@ -0,0 +1,30 @@ +import java +import semmle.code.java.dataflow.TaintTracking + +query predicate literalCall( + MethodCall call, Method target, string resultType, Expr qualifier, int argumentCount +) { + target = call.getMethod() and + target.hasName("of") and + call.getEnclosingCallable().fromSource() and + resultType = call.getType().toString() and + qualifier = call.getQualifier() and + argumentCount = call.getNumArgument() +} + +query predicate literalArguments(MethodCall call, int index, Expr argument) { + call.getMethod().hasName("of") and + argument = call.getArgument(index) +} + +module Config implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node n) { n.asExpr().(MethodCall).getMethod().hasName("source") } + + predicate isSink(DataFlow::Node n) { n.asExpr().(Argument).getCall().getCallee().hasName("sink") } +} + +module Flow = TaintTracking::Global; + +from DataFlow::Node source, DataFlow::Node sink +where Flow::flow(source, sink) +select source, sink diff --git a/java/ql/test-kotlin2/library-tests/collection-literal-operators/test.qlref b/java/ql/test-kotlin2/library-tests/collection-literal-operators/test.qlref new file mode 100644 index 000000000000..b79433a5b459 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/collection-literal-operators/test.qlref @@ -0,0 +1 @@ +test.ql From 7015661ed7b4f267861a89d3f04163f1395141fb Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 28 Aug 2026 14:53:25 +0200 Subject: [PATCH 07/10] Kotlin: cover companion blocks and extensions Add focused coverage for a companion-block function and companion extension function and property. Check declaration ownership, property accessors, resolved calls, and data flow. The existing extractor handles the lowered declarations and calls correctly. This commit adds coverage only and requires no extractor change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../companion-extensions/test.expected | 11 +++++ .../companion-extensions/test.kt | 21 +++++++++ .../companion-extensions/test.ql | 43 +++++++++++++++++++ .../companion-extensions/test.qlref | 1 + 4 files changed, 76 insertions(+) create mode 100644 java/ql/test-kotlin2/library-tests/companion-extensions/test.expected create mode 100644 java/ql/test-kotlin2/library-tests/companion-extensions/test.kt create mode 100644 java/ql/test-kotlin2/library-tests/companion-extensions/test.ql create mode 100644 java/ql/test-kotlin2/library-tests/companion-extensions/test.qlref diff --git a/java/ql/test-kotlin2/library-tests/companion-extensions/test.expected b/java/ql/test-kotlin2/library-tests/companion-extensions/test.expected new file mode 100644 index 000000000000..0eed1f5dced2 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/companion-extensions/test.expected @@ -0,0 +1,11 @@ +declarations +| test.kt:5:9:5:29 | empty | test.kt:3:1:7:1 | Box | Method | empty() | +| test.kt:9:11:9:52 | create | test.kt:0:0:0:0 | TestKt | Method | create(java.lang.String) | +| test.kt:12:5:12:19 | getDefault | test.kt:0:0:0:0 | TestKt | Method | getDefault() | +calls +| test.kt:19:14:19:29 | create(...) | test.kt:18:1:21:1 | test | test.kt:9:11:9:52 | create | test.kt:19:14:19:29 | TestKt | +| test.kt:20:14:20:20 | getDefault(...) | test.kt:18:1:21:1 | test | test.kt:12:5:12:19 | getDefault | test.kt:20:14:20:20 | TestKt | +properties +| test.kt:11:11:12:19 | default | test.kt:0:0:0:0 | TestKt | test.kt:12:5:12:19 | getDefault | +#select +| test.kt:19:21:19:28 | source(...) | test.kt:19:14:19:35 | getValue(...) | diff --git a/java/ql/test-kotlin2/library-tests/companion-extensions/test.kt b/java/ql/test-kotlin2/library-tests/companion-extensions/test.kt new file mode 100644 index 000000000000..db79f5f9d000 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/companion-extensions/test.kt @@ -0,0 +1,21 @@ +// codeql-extractor-kotlin-options: -language-version 2.4 -Xcompanion-blocks-and-extensions + +class Box(val value: String) { + companion { + fun empty() = Box("") + } +} + +companion fun Box.create(value: String) = Box(value) + +companion val Box.default: Box + get() = Box("") + +fun source(): String = "" + +fun sink(value: String) {} + +fun test() { + sink(Box.create(source()).value) + sink(Box.default.value) +} diff --git a/java/ql/test-kotlin2/library-tests/companion-extensions/test.ql b/java/ql/test-kotlin2/library-tests/companion-extensions/test.ql new file mode 100644 index 000000000000..f28b76761c02 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/companion-extensions/test.ql @@ -0,0 +1,43 @@ +import java +import semmle.code.java.dataflow.TaintTracking + +predicate isCompanionCallable(Callable callable) { + callable.getName() = ["empty", "create", "getDefault"] +} + +query predicate declarations( + Callable callable, RefType declaringType, string primaryClass, string signature +) { + isCompanionCallable(callable) and + callable.fromSource() and + declaringType = callable.getDeclaringType() and + primaryClass = callable.getAPrimaryQlClass() and + signature = callable.getSignature() +} + +query predicate calls(MethodCall call, Callable caller, Method target, Expr qualifier) { + caller.fromSource() and + call.getEnclosingCallable() = caller and + target = call.getMethod() and + isCompanionCallable(target) and + qualifier = call.getQualifier() +} + +query predicate properties(Property property, RefType declaringType, Method getter) { + property.hasName("default") and + property.fromSource() and + getter = property.getGetter() and + declaringType = getter.getDeclaringType() +} + +module Config implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node n) { n.asExpr().(MethodCall).getMethod().hasName("source") } + + predicate isSink(DataFlow::Node n) { n.asExpr().(Argument).getCall().getCallee().hasName("sink") } +} + +module Flow = TaintTracking::Global; + +from DataFlow::Node source, DataFlow::Node sink +where Flow::flow(source, sink) +select source, sink diff --git a/java/ql/test-kotlin2/library-tests/companion-extensions/test.qlref b/java/ql/test-kotlin2/library-tests/companion-extensions/test.qlref new file mode 100644 index 000000000000..b79433a5b459 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/companion-extensions/test.qlref @@ -0,0 +1 @@ +test.ql From dedba973801b7abf8c7a15d2bf5be33a73e7057d Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 28 Aug 2026 14:55:51 +0200 Subject: [PATCH 08/10] Kotlin: cover invokedynamic when extraction Add a JVM 21 regression test for when generation using invokedynamic. The same expected source AST also passes with inline when generation. The backend choice does not change the IR observed by the extractor. This commit adds coverage only and requires no extractor change. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../library-tests/when-indy/PrintAst.expected | 38 +++++++++++++++++++ .../library-tests/when-indy/PrintAst.qlref | 1 + .../library-tests/when-indy/test.kt | 8 ++++ 3 files changed, 47 insertions(+) create mode 100644 java/ql/test-kotlin2/library-tests/when-indy/PrintAst.expected create mode 100644 java/ql/test-kotlin2/library-tests/when-indy/PrintAst.qlref create mode 100644 java/ql/test-kotlin2/library-tests/when-indy/test.kt diff --git a/java/ql/test-kotlin2/library-tests/when-indy/PrintAst.expected b/java/ql/test-kotlin2/library-tests/when-indy/PrintAst.expected new file mode 100644 index 000000000000..9ba7f92457e4 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/when-indy/PrintAst.expected @@ -0,0 +1,38 @@ +test.kt: +# 0| [CompilationUnit] test +# 0| 1: [Class] TestKt +# 3| 1: [Method] classify +# 3| 3: [TypeAccess] int +#-----| 4: (Parameters) +# 3| 0: [Parameter] value +# 3| 0: [TypeAccess] Object +# 4| 5: [BlockStmt] { ... } +# 8| 0: [ReturnStmt] return ... +# 4| 0: [StmtExpr] +# 4| 0: [BlockStmt] { ... } +# 4| 0: [LocalVariableDeclStmt] var ...; +# 4| 1: [LocalVariableDeclExpr] tmp0_subject +# 4| 0: [VarAccess] value +# 4| 1: [ExprStmt] ; +# 4| 0: [WhenExpr] when ... +# 5| 0: [WhenBranch] ... -> ... +# 5| 0: [InstanceOfExpr] ...instanceof... +# 5| 0: [VarAccess] tmp0_subject +# 5| 1: [TypeAccess] String +# 5| 1: [ExprStmt] ; +# 5| 0: [MethodCall] length(...) +# 5| -1: [ImplicitCastExpr] +# 5| 0: [TypeAccess] String +# 5| 1: [VarAccess] value +# 6| 1: [WhenBranch] ... -> ... +# 6| 0: [InstanceOfExpr] ...instanceof... +# 6| 0: [VarAccess] tmp0_subject +# 6| 1: [TypeAccess] int +# 6| 1: [ExprStmt] ; +# 6| 0: [ImplicitCastExpr] +# 6| 0: [TypeAccess] int +# 6| 1: [VarAccess] value +# 7| 2: [WhenBranch] ... -> ... +# 7| 0: [BooleanLiteral] true +# 7| 1: [ExprStmt] ; +# 7| 0: [IntegerLiteral] -1 diff --git a/java/ql/test-kotlin2/library-tests/when-indy/PrintAst.qlref b/java/ql/test-kotlin2/library-tests/when-indy/PrintAst.qlref new file mode 100644 index 000000000000..f391eb5e4636 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/when-indy/PrintAst.qlref @@ -0,0 +1 @@ +semmle/code/java/PrintAst.ql diff --git a/java/ql/test-kotlin2/library-tests/when-indy/test.kt b/java/ql/test-kotlin2/library-tests/when-indy/test.kt new file mode 100644 index 000000000000..4ca9734be110 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/when-indy/test.kt @@ -0,0 +1,8 @@ +// codeql-extractor-kotlin-options: -language-version 2.4 -jvm-target 21 -Xwhen-expressions=indy + +fun classify(value: Any): Int = + when (value) { + is String -> value.length + is Int -> value + else -> -1 + } From aa583f8cd4c2bd930692ef485c3d9296701e2f93 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 28 Aug 2026 15:09:10 +0200 Subject: [PATCH 09/10] Kotlin: keep full value class coverage focused Replace the secondary constructor's Long conversion with a String overload. This preserves coverage for non-trivial value-class construction without exercising an unrelated primitive-conversion diagnostic. This is a test-only refinement and does not change extractor behaviour. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../library-tests/full-value-classes/test.expected | 6 +++--- .../test-kotlin2/library-tests/full-value-classes/test.kt | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/java/ql/test-kotlin2/library-tests/full-value-classes/test.expected b/java/ql/test-kotlin2/library-tests/full-value-classes/test.expected index 5a4f96bdaee8..b72a61ccbe92 100644 --- a/java/ql/test-kotlin2/library-tests/full-value-classes/test.expected +++ b/java/ql/test-kotlin2/library-tests/full-value-classes/test.expected @@ -10,8 +10,8 @@ properties constructors | test.kt:3:1:5:1 | Base | Base() | | test.kt:7:22:7:65 | PairValue | PairValue(int,java.lang.String) | -| test.kt:8:5:8:68 | PairValue | PairValue(long) | +| test.kt:8:5:8:58 | PairValue | PairValue(java.lang.String) | constructorCalls | test.kt:7:1:9:1 | super(...) | test.kt:3:1:5:1 | Base | -| test.kt:8:32:8:68 | this(...) | test.kt:7:22:7:65 | PairValue | -| test.kt:11:40:11:55 | new PairValue(...) | test.kt:8:5:8:68 | PairValue | +| test.kt:8:34:8:58 | this(...) | test.kt:7:22:7:65 | PairValue | +| test.kt:11:42:11:57 | new PairValue(...) | test.kt:8:5:8:58 | PairValue | diff --git a/java/ql/test-kotlin2/library-tests/full-value-classes/test.kt b/java/ql/test-kotlin2/library-tests/full-value-classes/test.kt index 50f8209367a8..52c8163da134 100644 --- a/java/ql/test-kotlin2/library-tests/full-value-classes/test.kt +++ b/java/ql/test-kotlin2/library-tests/full-value-classes/test.kt @@ -5,7 +5,7 @@ abstract value class Base { } value class PairValue(override val value: Int, val label: String) : Base() { - constructor(value: Long) : this(value.toInt(), value.toString()) + constructor(value: String) : this(value.length, value) } -fun makePairValue(value: Long): Base = PairValue(value) +fun makePairValue(value: String): Base = PairValue(value) From 701a005d0d81b92fe586e269c4d266f0d0466e8a Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 28 Aug 2026 15:09:10 +0200 Subject: [PATCH 10/10] Kotlin: assign stable context parameter indices Derive Kotlin 2.4 parameter indices from preceding context and regular parameters while reserving the Java parameter slot for an extension receiver. This removes negative context-parameter indices and prevents setter parameter label collisions found by the consistency checks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../v_2_4_0/parameterIndexExcludingReceivers.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_4_0/parameterIndexExcludingReceivers.kt b/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_4_0/parameterIndexExcludingReceivers.kt index 5e9b384b47e5..60d899742158 100644 --- a/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_4_0/parameterIndexExcludingReceivers.kt +++ b/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_4_0/parameterIndexExcludingReceivers.kt @@ -5,9 +5,15 @@ import org.jetbrains.kotlin.ir.declarations.IrParameterKind import org.jetbrains.kotlin.ir.declarations.IrValueParameter fun parameterIndexExcludingReceivers(vp: IrValueParameter): Int { - val offset = - (vp.parent as? IrFunction)?.let { f -> - f.parameters.count { it.kind == IrParameterKind.DispatchReceiver || it.kind == IrParameterKind.ExtensionReceiver || it.kind == IrParameterKind.Context } - } ?: 0 - return vp.indexInParameters - offset + if ( + vp.kind == IrParameterKind.DispatchReceiver || + vp.kind == IrParameterKind.ExtensionReceiver + ) { + return -1 + } + return (vp.parent as? IrFunction) + ?.parameters + ?.take(vp.indexInParameters) + ?.count { it.kind == IrParameterKind.Context || it.kind == IrParameterKind.Regular } + ?: vp.indexInParameters }