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

Incremental: Support top level callables in libs #1707

Merged
merged 2 commits into from
Feb 2, 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
9 changes: 9 additions & 0 deletions api/api.base
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ package com.google.devtools.ksp.processing {
method public void associate(@NonNull java.util.List<? extends com.google.devtools.ksp.symbol.KSFile> sources, @NonNull String packageName, @NonNull String fileName, @NonNull String extensionName = "kt");
method public void associateByPath(@NonNull java.util.List<? extends com.google.devtools.ksp.symbol.KSFile> sources, @NonNull String path, @NonNull String extensionName = "kt");
method public void associateWithClasses(@NonNull java.util.List<? extends com.google.devtools.ksp.symbol.KSClassDeclaration> classes, @NonNull String packageName, @NonNull String fileName, @NonNull String extensionName = "kt");
method public void associateWithFunctions(@NonNull java.util.List<? extends com.google.devtools.ksp.symbol.KSFunctionDeclaration> functions, @NonNull String packageName, @NonNull String fileName, @NonNull String extensionName = "kt");
method public void associateWithProperties(@NonNull java.util.List<? extends com.google.devtools.ksp.symbol.KSPropertyDeclaration> properties, @NonNull String packageName, @NonNull String fileName, @NonNull String extensionName = "kt");
method @NonNull public java.io.OutputStream createNewFile(@NonNull com.google.devtools.ksp.processing.Dependencies dependencies, @NonNull String packageName, @NonNull String fileName, @NonNull String extensionName = "kt");
method @NonNull public java.io.OutputStream createNewFileByPath(@NonNull com.google.devtools.ksp.processing.Dependencies dependencies, @NonNull String path, @NonNull String extensionName = "kt");
method @NonNull public java.util.Collection<java.io.File> getGeneratedFile();
Expand All @@ -83,6 +85,13 @@ package com.google.devtools.ksp.processing {
property @NonNull public final com.google.devtools.ksp.processing.Dependencies ALL_FILES;
}

public enum ExitCode {
method @NonNull public static com.google.devtools.ksp.processing.ExitCode valueOf(String value) throws java.lang.IllegalArgumentException, java.lang.NullPointerException;
method @NonNull public static com.google.devtools.ksp.processing.ExitCode[] values();
enum_constant public static final com.google.devtools.ksp.processing.ExitCode OK;
enum_constant public static final com.google.devtools.ksp.processing.ExitCode PROCESSING_ERROR;
}

public interface JsPlatformInfo extends com.google.devtools.ksp.processing.PlatformInfo {
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,42 @@ interface CodeGenerator {
)

val generatedFile: Collection<File>

/**
* Associate [functions] to an output file.
*
* @param functions are [KSFunctionDeclaration]s from which this output is built. Only those that are obtained
* directly from [Resolver] are required.
* @param packageName corresponds to the relative path of the generated file; using either '.'or '/' as separator.
* @param fileName file name
* @param extensionName If "kt" or "java", this file will participate in subsequent compilation.
* Otherwise its creation is only considered in incremental processing.
* @see [CodeGenerator] for more details.
*/
fun associateWithFunctions(
functions: List<KSFunctionDeclaration>,
packageName: String,
fileName: String,
extensionName: String = "kt"
)

/**
* Associate [properties] to an output file.
*
* @param properties are [KSPropertyDeclaration]s from which this output is built. Only those that are obtained
* directly from [Resolver] are required.
* @param packageName corresponds to the relative path of the generated file; using either '.'or '/' as separator.
* @param fileName file name
* @param extensionName If "kt" or "java", this file will participate in subsequent compilation.
* Otherwise its creation is only considered in incremental processing.
* @see [CodeGenerator] for more details.
*/
fun associateWithProperties(
properties: List<KSPropertyDeclaration>,
packageName: String,
fileName: String,
extensionName: String = "kt"
)
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import com.google.devtools.ksp.processing.CodeGenerator
import com.google.devtools.ksp.processing.Dependencies
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFile
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
Expand Down Expand Up @@ -98,6 +100,42 @@ class CodeGeneratorImpl(
associate(files, path, extensionToDirectory(extensionName))
}

// FIXME: associate with the containing FileKt.
// The containing FileKt is unfortunately a backend thing and is not available in Kotlin metadata.
// Therefore, the only way to get it seems to be traversing bytecode and find the functions of interest.
//
// This implementation associates anyChangesWildcard, which is an overkill.
override fun associateWithFunctions(
functions: List<KSFunctionDeclaration>,
packageName: String,
fileName: String,
extensionName: String
) {
val path = pathOf(packageName, fileName, extensionName)
val files = functions.map {
it.containingFile ?: anyChangesWildcard
}
associate(files, path, extensionToDirectory(extensionName))
}

// FIXME: associate with the containing FileKt.
// The containing FileKt is unfortunately a backend thing and is not available in Kotlin metadata.
// Therefore, the only way to get it seems to be traversing bytecode and find the functions of interest.
//
// This implementation associates anyChangesWildcard, which is an overkill.
override fun associateWithProperties(
properties: List<KSPropertyDeclaration>,
packageName: String,
fileName: String,
extensionName: String
) {
val path = pathOf(packageName, fileName, extensionName)
val files = properties.map {
it.containingFile ?: anyChangesWildcard
}
associate(files, path, extensionToDirectory(extensionName))
}

private fun extensionToDirectory(extensionName: String): File {
return when (extensionName) {
"class" -> classDir
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,98 @@ class IncrementalCPIT(val useKSP2: Boolean) {
}
}

val func2Dirty = listOf(
"l1/src/main/kotlin/p1/TopFunc1.kt" to setOf(
"w: [ksp] processing done",
),
)

@Test
fun testCPChangesForFunctions() {
val gradleRunner = GradleRunner.create().withProjectDir(project.root)

gradleRunner.withArguments("clean", "assemble").build().let { result ->
Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":workload:assemble")?.outcome)
}

// Dummy changes
func2Dirty.forEach { (src, expectedDirties) ->
File(project.root, src).appendText("\n\n")
gradleRunner.withArguments("assemble").build().let { result ->
// Trivial changes should not result in re-processing.
Assert.assertEquals(TaskOutcome.UP_TO_DATE, result.task(":workload:kspKotlin")?.outcome)
}
}

// Value changes
func2Dirty.forEach { (src, expectedDirties) ->
File(project.root, src).writeText("package p1\n\nfun MyTopFunc1(): Int = 1")
gradleRunner.withArguments("assemble").withDebug(true).build().let { result ->
// Value changes should not result in re-processing.
Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":workload:kspKotlin")?.outcome)
val dirties = result.output.lines().filter { it.startsWith("w: [ksp]") }.toSet()
// Non-signature changes should not affect anything.
Assert.assertEquals(emptyMessage, dirties)
}
}

// Signature changes
func2Dirty.forEach { (src, expectedDirties) ->
File(project.root, src).writeText("package p1\n\nfun MyTopFunc1(): Double = 1.0")
gradleRunner.withArguments("assemble").build().let { result ->
Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":workload:kspKotlin")?.outcome)
val dirties = result.output.lines().filter { it.startsWith("w: [ksp]") }.toSet()
Assert.assertEquals(expectedDirties, dirties)
}
}
}

val prop2Dirty = listOf(
"l1/src/main/kotlin/p1/TopProp1.kt" to setOf(
"w: [ksp] processing done",
),
)

@Test
fun testCPChangesForProperties() {
val gradleRunner = GradleRunner.create().withProjectDir(project.root)

gradleRunner.withArguments("clean", "assemble").build().let { result ->
Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":workload:assemble")?.outcome)
}

// Dummy changes
prop2Dirty.forEach { (src, expectedDirties) ->
File(project.root, src).appendText("\n\n")
gradleRunner.withArguments("assemble").build().let { result ->
// Trivial changes should not result in re-processing.
Assert.assertEquals(TaskOutcome.UP_TO_DATE, result.task(":workload:kspKotlin")?.outcome)
}
}

// Value changes
prop2Dirty.forEach { (src, expectedDirties) ->
File(project.root, src).writeText("package p1\n\nval MyTopProp1: Int = 1")
gradleRunner.withArguments("assemble").withDebug(true).build().let { result ->
// Value changes should not result in re-processing.
Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":workload:kspKotlin")?.outcome)
val dirties = result.output.lines().filter { it.startsWith("w: [ksp]") }.toSet()
// Non-signature changes should not affect anything.
Assert.assertEquals(emptyMessage, dirties)
}
}

// Signature changes
prop2Dirty.forEach { (src, expectedDirties) ->
File(project.root, src).writeText("package p1\n\nval MyTopProp1: Double = 1.0")
gradleRunner.withArguments("assemble").build().let { result ->
Assert.assertEquals(TaskOutcome.SUCCESS, result.task(":workload:kspKotlin")?.outcome)
val dirties = result.output.lines().filter { it.startsWith("w: [ksp]") }.toSet()
Assert.assertEquals(expectedDirties, dirties)
}
}
}

private fun toggleFlags(vararg extras: String) {
val gradleRunner = GradleRunner.create().withProjectDir(project.root).withDebug(true)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package p1

fun MyTopFunc1(): Int = 0
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package p1

val MyTopProp1: Int = 0
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import com.google.devtools.ksp.getClassDeclarationByName
import com.google.devtools.ksp.getFunctionDeclarationsByName
import com.google.devtools.ksp.getPropertyDeclarationByName
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
import com.google.devtools.ksp.validate
Expand Down Expand Up @@ -61,6 +63,16 @@ class Validator : SymbolProcessor {
val l5 = resolver.getClassDeclarationByName("p1.L5")!!
codeGenerator.createNewFile(Dependencies(false), "p1", "l5", "log")
codeGenerator.associateWithClasses(listOf(l5), "p1", "l5", "log")

// create an output from MyTopFunc1, declared in TopFunc1.kt
val myTopFunc1 = resolver.getFunctionDeclarationsByName("p1.MyTopFunc1", true).single()
codeGenerator.createNewFile(Dependencies(false), "p1", "MyTopFunc1", "log")
codeGenerator.associateWithFunctions(listOf(myTopFunc1), "p1", "MyTopFunc1", "log")

// create an output from MyTopProp1, declared in TopFunc1.kt
val myTopProp1 = resolver.getPropertyDeclarationByName("p1.MyTopProp1", true)!!
codeGenerator.createNewFile(Dependencies(false), "p1", "MyTopProp1", "log")
codeGenerator.associateWithProperties(listOf(myTopProp1), "p1", "MyTopProp1", "log")
logger.warn("processing done")

processed = true
Expand Down