Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: kotlin schema extractor fixes #940

Merged
merged 1 commit into from Feb 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/kotlin/pom.xml
Expand Up @@ -184,6 +184,7 @@
<disableDefaultRuleSets>true</disableDefaultRuleSets>
<classPath>${generated.classpath}</classPath>
<jvmTarget>${java.version}</jvmTarget>
<jdkHome>${java.home}</jdkHome>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super bizarre, you'd think it would be able to figure this out itself...

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i know 😢 thankfully i came across this issue, which pointed me to this solution

<config>${project.build.directory}/detekt.yml</config>
<plugins>
<plugin>
Expand Down
Expand Up @@ -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
Expand All @@ -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.*
Expand All @@ -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
Expand Down Expand Up @@ -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<PsiComment>()
.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.
Expand All @@ -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"
}
Expand Down Expand Up @@ -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()) {
Expand All @@ -489,7 +511,7 @@ class SchemaExtractor(
"but was ${this.fqNameOrNull()?.asString()}"
}

return Type(
Type(
dataRef = DataRef(
name = refName,
module = fqName.extractModuleName(),
Expand All @@ -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 =
Expand Down
Expand Up @@ -54,7 +54,11 @@ internal class ExtractSchemaRuleTest(private val env: KotlinCoreEnvironment) {
/**
* Request to echo a message.
*/
data class EchoRequest<T>(val t: T, val name: String, @Alias("stf") val stuff: Any)
data class EchoRequest<T>(
val t: T,
val name: String,
@Alias("stf") val stuff: Any,
)
data class EchoResponse(val messages: List<EchoMessage>)

/**
Expand All @@ -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())
}
"""
Expand Down Expand Up @@ -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"
)
)
)
)
)
),
)
),
),
Expand Down
1 change: 1 addition & 0 deletions kotlin-runtime/scaffolding/pom.xml
Expand Up @@ -185,6 +185,7 @@
<disableDefaultRuleSets>true</disableDefaultRuleSets>
<classPath>${generated.classpath}</classPath>
<jvmTarget>${java.version}</jvmTarget>
<jdkHome>${java.home}</jdkHome>
<config>${project.build.directory}/detekt.yml</config>
<plugins>
<plugin>
Expand Down