Skip to content

Commit

Permalink
Use FIR to dump ZiplineService declarations (#1049)
Browse files Browse the repository at this point in the history
* Use FIR to dump ZiplineService declarations

I considered using KSP but it's awkard to generate artifacts
from KSP during compile that the compile task doesn't itself need.

Working towards #1047

* Spotless

* Rename things to FIR
  • Loading branch information
swankjesse committed Jun 21, 2023
1 parent b3f244d commit fd33c34
Show file tree
Hide file tree
Showing 12 changed files with 568 additions and 23 deletions.
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", v
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect" }
kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test" }
kotlin-compilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinx-serialization" }
Expand Down
21 changes: 21 additions & 0 deletions zipline-kotlin-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import com.vanniktech.maven.publish.JavadocJar
import com.vanniktech.maven.publish.KotlinJvm
import com.vanniktech.maven.publish.MavenPublishBaseExtension
import org.gradle.api.plugins.JavaPlugin.TEST_TASK_NAME
import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.TEST_COMPILATION_NAME

plugins {
kotlin("jvm")
Expand All @@ -15,6 +17,25 @@ dependencies {

kapt(libs.auto.service.compiler)
compileOnly(libs.auto.service.annotations)

testImplementation(projects.zipline)
testImplementation(libs.assertk)
testImplementation(libs.junit)
testImplementation(libs.kotlin.test)
testImplementation(kotlin("compiler-embeddable"))
}

// In order to simplify writing test schemas, inject the test sources and
// test classpath as properties into the test runtime. This allows testing
// the FIR-based parser on sources written inside the test case. Cool!
tasks.named<Test>(TEST_TASK_NAME).configure {
val compilation = kotlin.target.compilations.getByName(TEST_COMPILATION_NAME)

val sources = compilation.defaultSourceSet.kotlin.sourceDirectories.files
systemProperty("zipline.internal.sources", sources.joinToString(separator = File.pathSeparator))

val classpath = project.configurations.getByName(compilation.compileDependencyConfigurationName).files
systemProperty("zipline.internal.classpath", classpath.joinToString(separator = File.pathSeparator))
}

buildConfig {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (C) 2023 Cash App
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.zipline.api.fir

import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.declarations.FirClass
import org.jetbrains.kotlin.fir.declarations.FirRegularClass
import org.jetbrains.kotlin.fir.resolve.firClassLike

/** Recursively collect all supertypes of this class. */
internal fun FirClass.getAllSupertypes(session: FirSession): Set<FirClass> {
val result = mutableSetOf<FirClass>()
collectSupertypes(session, this, result)
return result
}

private fun collectSupertypes(
session: FirSession,
type: FirClass,
sink: MutableSet<FirClass>,
) {
if (!sink.add(type)) return // Already added.
if (type !is FirRegularClass) return // Cannot traverse supertypes.

val supertypes = type.symbol.resolvedSuperTypeRefs.mapNotNull {
it.firClassLike(session) as? FirClass
}

for (supertype in supertypes) {
collectSupertypes(session, supertype, sink)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Copyright (C) 2023 Cash App
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.zipline.api.fir

data class FirZiplineApi(
val services: List<FirZiplineService>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/*
* Copyright (C) 2023 Cash App
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.zipline.api.fir

import app.cash.zipline.kotlin.BridgedInterface.Companion.NON_INTERFACE_FUNCTION_NAMES
import app.cash.zipline.kotlin.FqPackageName
import app.cash.zipline.kotlin.classId
import java.io.File
import org.jetbrains.kotlin.fir.FirSession
import org.jetbrains.kotlin.fir.analysis.checkers.isSupertypeOf
import org.jetbrains.kotlin.fir.analysis.checkers.toRegularClassSymbol
import org.jetbrains.kotlin.fir.declarations.FirDeclaration
import org.jetbrains.kotlin.fir.declarations.FirFunction
import org.jetbrains.kotlin.fir.declarations.FirProperty
import org.jetbrains.kotlin.fir.declarations.FirRegularClass
import org.jetbrains.kotlin.fir.declarations.utils.isInterface
import org.jetbrains.kotlin.fir.declarations.utils.isSuspend
import org.jetbrains.kotlin.fir.pipeline.FirResult
import org.jetbrains.kotlin.fir.resolve.providers.symbolProvider
import org.jetbrains.kotlin.fir.symbols.impl.FirClassLikeSymbol
import org.jetbrains.kotlin.fir.symbols.impl.FirClassSymbol
import org.jetbrains.kotlin.fir.types.FirResolvedTypeRef
import org.jetbrains.kotlin.fir.types.FirStarProjection
import org.jetbrains.kotlin.fir.types.FirTypeProjection
import org.jetbrains.kotlin.fir.types.FirTypeProjectionWithVariance
import org.jetbrains.kotlin.fir.types.FirTypeRef
import org.jetbrains.kotlin.fir.types.FirUserTypeRef
import org.jetbrains.kotlin.types.Variance

fun readFirZiplineApi(
sources: Collection<File>,
dependencies: Collection<File>,
): FirZiplineApi {
return KotlinFirLoader(sources, dependencies).use { loader ->
val output = loader.load("zipline-api-dump")
FirZiplineApiReader(output).read()
}
}

private val ziplineFqPackage = FqPackageName("app.cash.zipline")
private val ziplineServiceClassId = ziplineFqPackage.classId("ZiplineService")

/**
* Read the frontend intermediate representation of a program and emit its ZiplineService
* interfaces. These are subject to strict API compatibility requirements.
*/
internal class FirZiplineApiReader(
output: FirResult,
) {
private val platformOutput = output.platformOutput
private val session: FirSession = platformOutput.session

private val ziplineServiceClass: FirClassLikeSymbol<*>? =
session.symbolProvider.getClassLikeSymbolByClassId(ziplineServiceClassId)

fun read(): FirZiplineApi {
val types = platformOutput.fir
.flatMap { it.declarations.findRegularClassesRecursive() }
.filter { it.isInterface && it.isZiplineService }

val services = types
.map { it.asDeclaredZiplineService() }
.sortedBy { it.name }

return FirZiplineApi(services)
}

private val FirRegularClass.isZiplineService: Boolean
get() {
val ziplineServiceClssSymbol = ziplineServiceClass as? FirClassSymbol<*> ?: return false
return ziplineServiceClssSymbol.isSupertypeOf(symbol, session)
}

private fun FirRegularClass.asDeclaredZiplineService(): FirZiplineService {
return FirZiplineService(
symbol.classId.asSingleFqName().asString(),
bridgedFunctions(this),
)
}

private fun bridgedFunctions(type: FirRegularClass): List<FirZiplineFunction> {
val result = sortedSetOf<FirZiplineFunction>(
{ a, b -> a.signature.compareTo(b.signature) },
)

for (supertype in type.getAllSupertypes(session)) {
if (!supertype.isInterface) continue // Skip kotlin.Any.

for (declaration in supertype.declarations) {
when (declaration) {
is FirFunction -> {
if (declaration.isNonInterfaceFunction) continue
result += declaration.asDeclaredZiplineFunction()
}

is FirProperty -> {
result += declaration.asDeclaredZiplineFunction()
}

else -> Unit
}
}
}

return result.toList()
}

private val FirFunction.isNonInterfaceFunction: Boolean
get() = symbol.name.identifier in NON_INTERFACE_FUNCTION_NAMES

private fun FirFunction.asDeclaredZiplineFunction(): FirZiplineFunction {
val signature = buildString {
if (isSuspend) append("suspend ")
append("fun ${symbol.name.identifier}(")
append(valueParameters.joinToString { it.returnTypeRef.asString() })
append("): ${returnTypeRef.asString()}")
}

return FirZiplineFunction(signature)
}

private fun FirProperty.asDeclaredZiplineFunction(): FirZiplineFunction {
val signature = when {
isVar -> "var ${symbol.name.identifier}: ${returnTypeRef.asString()}"
else -> "val ${symbol.name.identifier}: ${returnTypeRef.asString()}"
}
return FirZiplineFunction(signature)
}

/** See [app.cash.zipline.kotlin.asString]. */
private fun FirTypeRef.asString(): String {
val classSymbol = toRegularClassSymbol(session) ?: error("unexpected class: $this")

val typeRef = when (this) {
is FirResolvedTypeRef -> delegatedTypeRef ?: this
else -> this
}

return buildString {
append(classSymbol.classId.asSingleFqName().asString())

if (typeRef is FirUserTypeRef) {
val typeArguments = typeRef.qualifier.lastOrNull()?.typeArgumentList?.typeArguments
if (typeArguments?.isEmpty() == false) {
typeArguments.joinTo(this, separator = ",", prefix = "<", postfix = ">") {
it.asString()
}
}
}
}
}

private fun FirTypeProjection.asString(): String {
return when (this) {
is FirStarProjection -> {
"*"
}
is FirTypeProjectionWithVariance -> {
variance.label + (if (variance != Variance.INVARIANT) " " else "") + typeRef.asString()
}
else -> {
error("Unexpected kind of FirTypeProjection: " + javaClass.simpleName)
}
}
}

private fun List<FirDeclaration>.findRegularClassesRecursive(): List<FirRegularClass> {
val classes = filterIsInstance<FirRegularClass>()
return classes + classes.flatMap { it.declarations.findRegularClassesRecursive() }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 Cash App
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.zipline.api.fir

import app.cash.zipline.kotlin.signatureHash

data class FirZiplineFunction(
val id: String,
val signature: String,
) {
internal constructor(signature: String) : this(signature.signatureHash(), signature)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Copyright (C) 2023 Cash App
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package app.cash.zipline.api.fir

/** An interface that extends from ZiplineService. */
data class FirZiplineService(
val name: String,
val functions: List<FirZiplineFunction>,
)
Loading

0 comments on commit fd33c34

Please sign in to comment.