diff --git a/examples/kotlin/pom.xml b/examples/kotlin/pom.xml
index 4882a58a3e..89d5473b2a 100644
--- a/examples/kotlin/pom.xml
+++ b/examples/kotlin/pom.xml
@@ -184,6 +184,7 @@
true
${generated.classpath}
${java.version}
+ ${java.home}
${project.build.directory}/detekt.yml
diff --git a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRule.kt b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRule.kt
index fb87a3dc3c..6f8b631df8 100644
--- a/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRule.kt
+++ b/kotlin-runtime/ftl-runtime/src/main/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRule.kt
@@ -4,6 +4,7 @@ import io.gitlab.arturbosch.detekt.api.*
import io.gitlab.arturbosch.detekt.api.internal.RequiresTypeResolution
import io.gitlab.arturbosch.detekt.rules.fqNameOrNull
import org.jetbrains.kotlin.cfg.getDeclarationDescriptorIncludingConstructors
+import org.jetbrains.kotlin.com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.com.intellij.psi.PsiComment
import org.jetbrains.kotlin.com.intellij.psi.PsiElement
import org.jetbrains.kotlin.descriptors.ClassDescriptor
@@ -12,16 +13,25 @@ import org.jetbrains.kotlin.diagnostics.DiagnosticUtils.getLineAndColumnInPsiFil
import org.jetbrains.kotlin.diagnostics.PsiDiagnosticUtils.LineAndColumn
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.psi.*
+import org.jetbrains.kotlin.psi.psiUtil.children
import org.jetbrains.kotlin.psi.psiUtil.getValueParameters
+import org.jetbrains.kotlin.psi.psiUtil.startOffset
import org.jetbrains.kotlin.resolve.BindingContext
import org.jetbrains.kotlin.resolve.calls.util.getResolvedCall
import org.jetbrains.kotlin.resolve.calls.util.getType
+import org.jetbrains.kotlin.resolve.descriptorUtil.builtIns
+import org.jetbrains.kotlin.resolve.descriptorUtil.fqNameSafe
import org.jetbrains.kotlin.resolve.source.getPsi
import org.jetbrains.kotlin.resolve.typeBinding.createTypeBindingForReturnType
import org.jetbrains.kotlin.types.KotlinType
+import org.jetbrains.kotlin.types.checker.SimpleClassicTypeSystemContext.getClassFqNameUnsafe
import org.jetbrains.kotlin.types.checker.SimpleClassicTypeSystemContext.isTypeParameterTypeConstructor
+import org.jetbrains.kotlin.types.checker.anySuperTypeConstructor
import org.jetbrains.kotlin.types.isNullable
+import org.jetbrains.kotlin.types.typeUtil.builtIns
import org.jetbrains.kotlin.types.typeUtil.isAny
+import org.jetbrains.kotlin.types.typeUtil.isAnyOrNullableAny
+import org.jetbrains.kotlin.types.typeUtil.isSubtypeOf
import org.jetbrains.kotlin.util.containingNonLocalDeclaration
import org.jetbrains.kotlin.utils.addToStdlib.ifNotEmpty
import xyz.block.ftl.*
@@ -34,6 +44,7 @@ import java.io.File
import java.io.FileOutputStream
import java.nio.file.Path
import java.time.OffsetDateTime
+import java.util.HashMap
import kotlin.Any
import kotlin.Boolean
import kotlin.String
@@ -347,6 +358,11 @@ class SchemaExtractor(
val funcSourcePos = func.getLineAndColumn()
val body =
requireNotNull(func.bodyExpression) { "$funcSourcePos Function body cannot be empty; was in ${func.name}" }
+
+ val commentRanges = body.node.children()
+ .filterIsInstance()
+ .map { it.textRange.shiftLeft(it.startOffset).shiftRight(it.startOffsetInParent) }
+
val imports = func.containingKtFile.importList?.imports ?: emptyList()
// Look for all params of type Context and extract a matcher for each based on its variable name.
@@ -356,7 +372,10 @@ class SchemaExtractor(
}.map { ctxParam -> getCallMatcher(ctxParam.text.split(":")[0].trim()) }
val refs = callMatchers.flatMap { matcher ->
- matcher.findAll(body.text).map { match ->
+ matcher.findAll(body.text).mapNotNull { match ->
+ // ignore commented out matches
+ if (commentRanges.any { it.contains(TextRange(match.range.first, match.range.last)) }) return@mapNotNull null
+
val verbCall = requireNotNull(match.groups["fn"]?.value?.substringAfter("::")?.trim()) {
"Error processing function defined at $funcSourcePos: Could not extract outgoing verb call"
}
@@ -447,33 +466,36 @@ class SchemaExtractor(
)
)
}
-
- val type = when (this.fqNameOrNull()?.asString()) {
- String::class.qualifiedName -> Type(string = xyz.block.ftl.v1.schema.String())
- Int::class.qualifiedName -> Type(int = xyz.block.ftl.v1.schema.Int())
- Long::class.qualifiedName -> Type(int = xyz.block.ftl.v1.schema.Int())
- Double::class.qualifiedName -> Type(float = xyz.block.ftl.v1.schema.Float())
- Boolean::class.qualifiedName -> Type(bool = xyz.block.ftl.v1.schema.Bool())
- OffsetDateTime::class.qualifiedName -> Type(time = xyz.block.ftl.v1.schema.Time())
- ByteArray::class.qualifiedName -> Type(bytes = xyz.block.ftl.v1.schema.Bytes())
- Any::class.qualifiedName -> Type(any = xyz.block.ftl.v1.schema.Any())
- Unit::class.qualifiedName -> Type(unit = xyz.block.ftl.v1.schema.Unit())
- Map::class.qualifiedName -> {
- return Type(
- map = xyz.block.ftl.v1.schema.Map(
- key = this.arguments.first().type.toSchemaType(position),
- value_ = this.arguments.last().type.toSchemaType(position),
- )
+ val builtIns = this.builtIns
+ val type = this.constructor.declarationDescriptor!!.defaultType
+ val schemaType = when {
+ type.isSubtypeOf(builtIns.stringType) -> Type(string = xyz.block.ftl.v1.schema.String())
+ type.isSubtypeOf(builtIns.intType) -> Type(int = xyz.block.ftl.v1.schema.Int())
+ type.isSubtypeOf(builtIns.longType) -> Type(int = xyz.block.ftl.v1.schema.Int())
+ type.isSubtypeOf(builtIns.doubleType) -> Type(float = xyz.block.ftl.v1.schema.Float())
+ type.isSubtypeOf(builtIns.booleanType) -> Type(bool = xyz.block.ftl.v1.schema.Bool())
+ type.isSubtypeOf(builtIns.byteType) -> Type(bytes = xyz.block.ftl.v1.schema.Bytes())
+ type.isSubtypeOf(builtIns.unitType) -> Type(unit = xyz.block.ftl.v1.schema.Unit())
+ type.anySuperTypeConstructor {
+ it.getClassFqNameUnsafe().asString() == builtIns.list.fqNameSafe.asString()
+ } -> Type(
+ array = Array(
+ element = this.arguments.first().type.toSchemaType(position)
)
- }
+ )
- List::class.qualifiedName -> {
- return Type(
- array = Array(
- element = this.arguments.first().type.toSchemaType(position)
- )
+ type.anySuperTypeConstructor {
+ it.getClassFqNameUnsafe().asString() == builtIns.map.fqNameSafe.asString()
+ } -> Type(
+ map = xyz.block.ftl.v1.schema.Map(
+ key = this.arguments.first().type.toSchemaType(position),
+ value_ = this.arguments.last().type.toSchemaType(position),
)
- }
+ )
+
+ this.isAnyOrNullableAny() -> Type(any = xyz.block.ftl.v1.schema.Any())
+ this.fqNameOrNull()
+ ?.asString() == OffsetDateTime::class.qualifiedName -> Type(time = xyz.block.ftl.v1.schema.Time())
else -> {
require(this.toClassDescriptor().isData || this.isEmptyBuiltin()) {
@@ -489,7 +511,7 @@ class SchemaExtractor(
"but was ${this.fqNameOrNull()?.asString()}"
}
- return Type(
+ Type(
dataRef = DataRef(
name = refName,
module = fqName.extractModuleName(),
@@ -500,12 +522,12 @@ class SchemaExtractor(
}
}
if (this.isNullable()) {
- return Type(optional = Optional(type = type))
+ return Type(optional = Optional(type = schemaType))
}
if (this.isAny()) {
return Type(any = xyz.block.ftl.v1.schema.Any())
}
- return type
+ return schemaType
}
private fun KtTypeReference.resolveType(): KotlinType =
diff --git a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRuleTest.kt b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRuleTest.kt
index 412ca2dc1b..dfc82521d5 100644
--- a/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRuleTest.kt
+++ b/kotlin-runtime/ftl-runtime/src/test/kotlin/xyz/block/ftl/schemaextractor/ExtractSchemaRuleTest.kt
@@ -54,7 +54,11 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) {
/**
* Request to echo a message.
*/
- data class EchoRequest(val t: T, val name: String, @Alias("stf") val stuff: Any)
+ data class EchoRequest(
+ val t: T,
+ val name: String,
+ @Alias("stf") val stuff: Any,
+ )
data class EchoResponse(val messages: List)
/**
@@ -81,6 +85,8 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) {
fun callTime(context: Context): TimeResponse {
context.call(::empty, builtin.Empty())
context.call(::other, builtin.Empty())
+ // commented out call is ignored:
+ //context.call(::foo, builtin.Empty())
return context.call(::verb, builtin.Empty())
}
"""
@@ -114,15 +120,19 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) {
Field(
name = "metadata",
type = Type(
- map = Map(
- key = Type(string = xyz.block.ftl.v1.schema.String()),
- value_ = Type(
- dataRef = DataRef(
- name = "MapValue",
- module = "echo"
+ optional = Optional(
+ type = Type(
+ map = Map(
+ key = Type(string = xyz.block.ftl.v1.schema.String()),
+ value_ = Type(
+ dataRef = DataRef(
+ name = "MapValue",
+ module = "echo"
+ )
+ )
)
)
- )
+ ),
)
),
),
diff --git a/kotlin-runtime/scaffolding/pom.xml b/kotlin-runtime/scaffolding/pom.xml
index b6785771cb..dfa1bb73ce 100644
--- a/kotlin-runtime/scaffolding/pom.xml
+++ b/kotlin-runtime/scaffolding/pom.xml
@@ -185,6 +185,7 @@
true
${generated.classpath}
${java.version}
+ ${java.home}
${project.build.directory}/detekt.yml