From 2f4571b083c7781ebcd008b86cc8f5912f7927bf Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Fri, 28 Aug 2026 14:15:05 +0200 Subject: [PATCH 01/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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/18] 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 } From 01235a25ece3dce7a731e86ee8e4a63aaf2e54aa Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Mon, 31 Aug 2026 09:32:31 +0200 Subject: [PATCH 11/18] Kotlin: gate full value class coverage Run the full multi-field value class fixture only with Kotlin 2.4.20-RC2 or later in K2 mode. Earlier supported compilers reject the tested syntax even with the FullValueClasses language feature enabled.\n\nThis is coverage metadata only. Existing extraction remains correct and no extractor change is required.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../library-tests/full-value-classes/codeql-test.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 java/ql/test-kotlin2/library-tests/full-value-classes/codeql-test.yml diff --git a/java/ql/test-kotlin2/library-tests/full-value-classes/codeql-test.yml b/java/ql/test-kotlin2/library-tests/full-value-classes/codeql-test.yml new file mode 100644 index 000000000000..4e0b2727282d --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/full-value-classes/codeql-test.yml @@ -0,0 +1,3 @@ +kotlin: + minimumVersion: 2.4.20-RC2 + languageMode: K2 From b514ee0d6147841d86f022c10b284f756460f6d4 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Mon, 31 Aug 2026 09:32:39 +0200 Subject: [PATCH 12/18] Kotlin: gate name-based destructuring coverage Run the short-form name-based destructuring fixture only with Kotlin 2.4.20-RC2 or later in K2 mode. Earlier supported compilers reject the tested combination of full value classes and name-based destructuring options.\n\nThis is coverage metadata only. Existing extraction remains correct and no extractor change is required.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../library-tests/name-based-destructuring/codeql-test.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 java/ql/test-kotlin2/library-tests/name-based-destructuring/codeql-test.yml diff --git a/java/ql/test-kotlin2/library-tests/name-based-destructuring/codeql-test.yml b/java/ql/test-kotlin2/library-tests/name-based-destructuring/codeql-test.yml new file mode 100644 index 000000000000..4e0b2727282d --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/name-based-destructuring/codeql-test.yml @@ -0,0 +1,3 @@ +kotlin: + minimumVersion: 2.4.20-RC2 + languageMode: K2 From 07783714e0838438aeac92d0fdd35233146aad9c Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Mon, 31 Aug 2026 09:32:46 +0200 Subject: [PATCH 13/18] Kotlin: gate context parameter coverage Run the function, getter, and setter context-parameter fixture from Kotlin 2.2.20-Beta2 onwards in K2 mode. This is the earliest supported compiler that accepts the exact fixture with -Xcontext-parameters.\n\nThe wider gate makes the existing regression test applicable to earlier compilers so their extraction behaviour can be assessed rather than assuming the fix is specific to Kotlin 2.4.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../library-tests/context-parameters/codeql-test.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 java/ql/test-kotlin2/library-tests/context-parameters/codeql-test.yml diff --git a/java/ql/test-kotlin2/library-tests/context-parameters/codeql-test.yml b/java/ql/test-kotlin2/library-tests/context-parameters/codeql-test.yml new file mode 100644 index 000000000000..a087b5d33b7d --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/context-parameters/codeql-test.yml @@ -0,0 +1,3 @@ +kotlin: + minimumVersion: 2.2.20-Beta2 + languageMode: K2 From a0cf379cdf9bb483876893c83f01db2ad7d094cc Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Mon, 31 Aug 2026 09:32:56 +0200 Subject: [PATCH 14/18] Kotlin: gate collection literal coverage Run the custom collection literal fixture from Kotlin 2.4.0 onwards in K2 mode. Earlier supported compilers reject the tested CollectionLiterals language feature.\n\nThis is coverage metadata only. Existing extraction remains correct and no extractor change is required.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../library-tests/collection-literal-operators/codeql-test.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 java/ql/test-kotlin2/library-tests/collection-literal-operators/codeql-test.yml diff --git a/java/ql/test-kotlin2/library-tests/collection-literal-operators/codeql-test.yml b/java/ql/test-kotlin2/library-tests/collection-literal-operators/codeql-test.yml new file mode 100644 index 000000000000..d9dcb71c2be8 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/collection-literal-operators/codeql-test.yml @@ -0,0 +1,3 @@ +kotlin: + minimumVersion: 2.4.0 + languageMode: K2 From 131ef8eb74d523c46ec28701653f0b16707624c0 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Mon, 31 Aug 2026 09:33:05 +0200 Subject: [PATCH 15/18] Kotlin: gate companion extension coverage Run the companion block and companion extension fixture only with Kotlin 2.4.20-RC2 or later in K2 mode. Earlier supported compilers reject -Xcompanion-blocks-and-extensions.\n\nThis is coverage metadata only. Existing extraction remains correct and no extractor change is required.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../library-tests/companion-extensions/codeql-test.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 java/ql/test-kotlin2/library-tests/companion-extensions/codeql-test.yml diff --git a/java/ql/test-kotlin2/library-tests/companion-extensions/codeql-test.yml b/java/ql/test-kotlin2/library-tests/companion-extensions/codeql-test.yml new file mode 100644 index 000000000000..4e0b2727282d --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/companion-extensions/codeql-test.yml @@ -0,0 +1,3 @@ +kotlin: + minimumVersion: 2.4.20-RC2 + languageMode: K2 From 8630d5af4d9d763e7540aeed239a8c505d56e85b Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Mon, 31 Aug 2026 09:33:13 +0200 Subject: [PATCH 16/18] Kotlin: gate invokedynamic when coverage Run the invokedynamic when-expression fixture from Kotlin 2.2.20-Beta2 onwards in K2 mode, and require Java 21 for its configured JVM target. Earlier supported compilers reject the tested backend option.\n\nThis is coverage metadata only. Existing source-level extraction remains correct and no extractor change is required.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- java/ql/test-kotlin2/library-tests/when-indy/codeql-test.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 java/ql/test-kotlin2/library-tests/when-indy/codeql-test.yml diff --git a/java/ql/test-kotlin2/library-tests/when-indy/codeql-test.yml b/java/ql/test-kotlin2/library-tests/when-indy/codeql-test.yml new file mode 100644 index 000000000000..027fb12c8523 --- /dev/null +++ b/java/ql/test-kotlin2/library-tests/when-indy/codeql-test.yml @@ -0,0 +1,4 @@ +kotlin: + minimumVersion: 2.2.20-Beta2 + languageMode: K2 + minimumJavaVersion: 21 From b0fab01ec33a10c8e262df779bbb558b786e1092 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Mon, 31 Aug 2026 09:44:01 +0200 Subject: [PATCH 17/18] Kotlin: extract context parameters from 2.2.20 Use the unified parameter and argument layout introduced in Kotlin 2.2.20-Beta2 so context parameters and their implicit call arguments are included before the old accessors disappear in Kotlin 2.4.\n\nAssign parameter indices after excluding receiver slots, and give compiler-generated context arguments a stable whole-file location when the earlier compiler supplies inconsistent offsets. This fixes the version-gated function, getter, and setter regression test without changing its expected output. Earlier compilers retain the legacy compatibility implementation because they do not accept the tested context-parameter syntax.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/main/kotlin/KotlinFileExtractor.kt | 87 ++++++++++++++----- .../v_1_8_0/isCodeQlContextParameter.kt | 5 ++ .../utils/versions/v_2_2_20-Beta2/IrCompat.kt | 82 +++++++++++++++++ .../isCodeQlContextParameter.kt | 6 ++ .../parameterIndexExcludingReceivers.kt | 19 ++++ 5 files changed, 178 insertions(+), 21 deletions(-) create mode 100644 java/kotlin-extractor/src/main/kotlin/utils/versions/v_1_8_0/isCodeQlContextParameter.kt create mode 100644 java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_2_20-Beta2/IrCompat.kt create mode 100644 java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_2_20-Beta2/isCodeQlContextParameter.kt create mode 100644 java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_2_20-Beta2/parameterIndexExcludingReceivers.kt diff --git a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt index bef2b554d7ec..a6c66523e89c 100644 --- a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt +++ b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt @@ -3590,7 +3590,9 @@ open class KotlinFileExtractor( valueArguments, enclosingStmt, enclosingCallable, - idxOffset + idxOffset, + valueParameters = syntacticCallTarget.codeQlValueParameters, + invalidArgumentLocation = tw.getWholeFileLocation() ) }, dispatchReceiver?.type, @@ -3804,14 +3806,17 @@ open class KotlinFileExtractor( enclosingStmt: Label, enclosingCallable: Label, idxOffset: Int - ) = + ) { extractCallValueArguments( callId, (0 until call.codeQlValueArgumentsCount).map { call.codeQlGetValueArgument(it) }, enclosingStmt, enclosingCallable, - idxOffset + idxOffset, + valueParameters = call.symbol.owner.codeQlValueParameters, + invalidArgumentLocation = tw.getWholeFileLocation() ) + } private fun extractCallValueArguments( callId: Label, @@ -3819,35 +3824,75 @@ open class KotlinFileExtractor( enclosingStmt: Label, enclosingCallable: Label, idxOffset: Int, - extractVarargAsArray: Boolean = false + extractVarargAsArray: Boolean = false, + valueParameters: List? = null, + invalidArgumentLocation: Label? = null ) { var i = 0 - valueArguments.forEach { arg -> + valueArguments.forEachIndexed { argumentIndex, arg -> if (arg != null) { - if (arg is IrVararg && !extractVarargAsArray) { - arg.elements.forEachIndexed { varargNo, vararg -> - extractVarargElement( - vararg, - enclosingCallable, - callId, - i + idxOffset + varargNo, - enclosingStmt - ) - } - i += arg.elements.size - } else { - extractExpressionExpr( - arg, - enclosingCallable, + val parameter = valueParameters?.getOrNull(argumentIndex) + if ( + parameter?.isCodeQlContextParameter() == true && + arg is IrGetValue && + (arg.startOffset < 0 || arg.endOffset < 0) && + invalidArgumentLocation != null + ) { + extractVariableAccess( + useValueDeclaration(arg.symbol.owner), + arg.type, + invalidArgumentLocation, callId, - (i++) + idxOffset, + i++ + idxOffset, + enclosingCallable, enclosingStmt ) + } else { + i += + extractCallValueArgument( + callId, + arg, + enclosingStmt, + enclosingCallable, + i + idxOffset, + extractVarargAsArray + ) } } } } + private fun extractCallValueArgument( + callId: Label, + argument: IrExpression, + enclosingStmt: Label, + enclosingCallable: Label, + outputIndex: Int, + extractVarargAsArray: Boolean = false + ): Int { + if (argument is IrVararg && !extractVarargAsArray) { + argument.elements.forEachIndexed { varargIndex, element -> + extractVarargElement( + element, + enclosingCallable, + callId, + outputIndex + varargIndex, + enclosingStmt + ) + } + return argument.elements.size + } + + extractExpressionExpr( + argument, + enclosingCallable, + callId, + outputIndex, + enclosingStmt + ) + return 1 + } + private fun findFunction(cls: IrClass, name: String): IrFunction? = cls.declarations.findSubType { it.name.asString() == name } diff --git a/java/kotlin-extractor/src/main/kotlin/utils/versions/v_1_8_0/isCodeQlContextParameter.kt b/java/kotlin-extractor/src/main/kotlin/utils/versions/v_1_8_0/isCodeQlContextParameter.kt new file mode 100644 index 000000000000..fb2e82ede34b --- /dev/null +++ b/java/kotlin-extractor/src/main/kotlin/utils/versions/v_1_8_0/isCodeQlContextParameter.kt @@ -0,0 +1,5 @@ +package com.github.codeql.utils.versions + +import org.jetbrains.kotlin.ir.declarations.IrValueParameter + +fun IrValueParameter.isCodeQlContextParameter() = false diff --git a/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_2_20-Beta2/IrCompat.kt b/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_2_20-Beta2/IrCompat.kt new file mode 100644 index 000000000000..7f157bcb5e14 --- /dev/null +++ b/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_2_20-Beta2/IrCompat.kt @@ -0,0 +1,82 @@ +package com.github.codeql.utils.versions + +import org.jetbrains.kotlin.ir.declarations.IrFunction +import org.jetbrains.kotlin.ir.declarations.IrMutableAnnotationContainer +import org.jetbrains.kotlin.ir.declarations.IrParameterKind +import org.jetbrains.kotlin.ir.declarations.IrValueParameter +import org.jetbrains.kotlin.ir.expressions.IrConstructorCall +import org.jetbrains.kotlin.ir.expressions.IrExpression +import org.jetbrains.kotlin.ir.expressions.IrMemberAccessExpression +import org.jetbrains.kotlin.ir.expressions.impl.IrConstructorCallImpl +import org.jetbrains.kotlin.ir.expressions.impl.fromSymbolOwner +import org.jetbrains.kotlin.ir.symbols.IrConstructorSymbol +import org.jetbrains.kotlin.ir.types.IrType +import org.jetbrains.kotlin.ir.types.addAnnotations + +private fun IrParameterKind.isCodeQlValueParameter() = + this == IrParameterKind.Context || this == IrParameterKind.Regular + +val IrFunction.codeQlValueParameters: List + get() = parameters.filter { it.kind.isCodeQlValueParameter() } + +val IrFunction.codeQlExtensionReceiverParameter: IrValueParameter? + get() = extensionReceiverParameter + +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() } + } +} + +val IrMemberAccessExpression<*>.codeQlValueArgumentsCount: Int + get() = valueArgumentIndices().size + +fun IrMemberAccessExpression<*>.codeQlGetValueArgument(index: Int): IrExpression? = + arguments[valueArgumentIndices()[index]] + +fun IrMemberAccessExpression<*>.codeQlPutValueArgument(index: Int, value: IrExpression?) { + arguments[valueArgumentIndices()[index]] = value +} + +val IrMemberAccessExpression<*>.codeQlExtensionReceiver: IrExpression? + get() = extensionReceiver + +val IrMemberAccessExpression<*>.codeQlTypeArgumentsCount: Int + get() = typeArgumentsCount + +fun IrMemberAccessExpression<*>.codeQlGetTypeArgument(index: Int): IrType? = getTypeArgument(index) + +fun IrType.codeQlAddAnnotations(annotations: List): IrType = + addAnnotations(annotations) + +fun codeQlSetAnnotations( + container: IrMutableAnnotationContainer, + annotations: List +) { + container.annotations = annotations +} + +fun IrFunction.codeQlSetDispatchReceiverParameter(param: IrValueParameter?) { + dispatchReceiverParameter = param +} + +fun codeQlAnnotationFromSymbolOwner( + startOffset: Int, + endOffset: Int, + type: IrType, + symbol: IrConstructorSymbol, + typeArgumentsCount: Int +): IrConstructorCall = + IrConstructorCallImpl.fromSymbolOwner( + startOffset, + endOffset, + type, + symbol, + typeArgumentsCount + ) + +fun codeQlAnnotationFromSymbolOwner( + type: IrType, + symbol: IrConstructorSymbol +): IrConstructorCall = IrConstructorCallImpl.fromSymbolOwner(type, symbol) diff --git a/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_2_20-Beta2/isCodeQlContextParameter.kt b/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_2_20-Beta2/isCodeQlContextParameter.kt new file mode 100644 index 000000000000..ae4900d722e5 --- /dev/null +++ b/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_2_20-Beta2/isCodeQlContextParameter.kt @@ -0,0 +1,6 @@ +package com.github.codeql.utils.versions + +import org.jetbrains.kotlin.ir.declarations.IrParameterKind +import org.jetbrains.kotlin.ir.declarations.IrValueParameter + +fun IrValueParameter.isCodeQlContextParameter() = kind == IrParameterKind.Context diff --git a/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_2_20-Beta2/parameterIndexExcludingReceivers.kt b/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_2_20-Beta2/parameterIndexExcludingReceivers.kt new file mode 100644 index 000000000000..60d899742158 --- /dev/null +++ b/java/kotlin-extractor/src/main/kotlin/utils/versions/v_2_2_20-Beta2/parameterIndexExcludingReceivers.kt @@ -0,0 +1,19 @@ +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 + +fun parameterIndexExcludingReceivers(vp: IrValueParameter): Int { + 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 +} From 44f0616d07c6966c40e2c765552fad2502241f0e Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Mon, 31 Aug 2026 09:48:06 +0200 Subject: [PATCH 18/18] Kotlin: stabilise implicit context argument locations Use the shared whole-file location for implicit context arguments across Kotlin 2.2.20-Beta2 through 2.4.20-RC2. Kotlin 2.3.20 reports broad source ranges for these generated reads while other versions report synthetic offsets, although the extracted argument semantics are identical.\n\nNormalising only implicit arguments to context parameters keeps one focused expected result across supported compilers without weakening the assertions or changing unrelated expression locations.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../src/main/kotlin/KotlinFileExtractor.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt index a6c66523e89c..808278a55031 100644 --- a/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt +++ b/java/kotlin-extractor/src/main/kotlin/KotlinFileExtractor.kt @@ -3592,7 +3592,7 @@ open class KotlinFileExtractor( enclosingCallable, idxOffset, valueParameters = syntacticCallTarget.codeQlValueParameters, - invalidArgumentLocation = tw.getWholeFileLocation() + contextArgumentLocation = tw.getWholeFileLocation() ) }, dispatchReceiver?.type, @@ -3814,7 +3814,7 @@ open class KotlinFileExtractor( enclosingCallable, idxOffset, valueParameters = call.symbol.owner.codeQlValueParameters, - invalidArgumentLocation = tw.getWholeFileLocation() + contextArgumentLocation = tw.getWholeFileLocation() ) } @@ -3826,7 +3826,7 @@ open class KotlinFileExtractor( idxOffset: Int, extractVarargAsArray: Boolean = false, valueParameters: List? = null, - invalidArgumentLocation: Label? = null + contextArgumentLocation: Label? = null ) { var i = 0 valueArguments.forEachIndexed { argumentIndex, arg -> @@ -3835,13 +3835,12 @@ open class KotlinFileExtractor( if ( parameter?.isCodeQlContextParameter() == true && arg is IrGetValue && - (arg.startOffset < 0 || arg.endOffset < 0) && - invalidArgumentLocation != null + contextArgumentLocation != null ) { extractVariableAccess( useValueDeclaration(arg.symbol.owner), arg.type, - invalidArgumentLocation, + contextArgumentLocation, callId, i++ + idxOffset, enclosingCallable,