Skip to content

Commit

Permalink
A new CodeGenerator.createFile method with more flexibility (#1041)
Browse files Browse the repository at this point in the history
* Support creating files with a more flexible API

* Change associate api to use extension

(cherry picked from commit 1b5864d)
  • Loading branch information
jameskleeh authored and KSP Auto Pick committed Aug 12, 2022
1 parent 2b8aa42 commit 857e994
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 29 deletions.
2 changes: 2 additions & 0 deletions api/api.base
Expand Up @@ -59,8 +59,10 @@ package com.google.devtools.ksp.processing {

public interface CodeGenerator {
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 @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();
property @NonNull public abstract java.util.Collection<java.io.File> generatedFile;
}
Expand Down
Expand Up @@ -63,6 +63,38 @@ interface CodeGenerator {
extensionName: String = "kt"
): OutputStream

/**
* Creates a file which is managed by [CodeGenerator]
*
* Sources of corresponding [KSNode]s which are obtained directly from [Resolver] need to be specified.
* Namely, the containing files of those [KSNode]s who are obtained from:
* * [Resolver.getAllFiles]
* * [Resolver.getSymbolsWithAnnotation]
* * [Resolver.getClassDeclarationByName]
*
* Instead of requiring processors to specify all source files which are relevant in generating the given output,
* KSP traces dependencies automatically and only needs to know those sources that only processors know what they
* are for. If a [KSFile] is indirectly obtained through other [KSNode]s, it hasn't to be specified for the given
* output, even if its contents contribute to the generation of the output.
*
* For example, a processor generates an output `O` after reading class `A` in `A.kt` and class `B` in `B.kt`,
* where `A` extends `B`. The processor got `A` by [Resolver.getSymbolsWithAnnotation] and then got `B` by
* [KSClassDeclaration.superTypes] from `A`. Because the inclusion of `B` is due to `A`, `B.kt` needn't to be
* specified in [dependencies] for `O`. Note that specifying `B.kt` in this case doesn't hurt, it is only unnecessary.
*
* @param dependencies are [KSFile]s from which this output is built. Only those that are obtained directly
* from [Resolver] are required.
* @param path corresponds to the relative path of the generated file; includes the full file name
* @param fileType determines the target directory to store the file
* @return OutputStream for writing into files.
* @see [CodeGenerator] for more details.
*/
fun createNewFileByPath(
dependencies: Dependencies,
path: String,
extensionName: String = "kt"
): OutputStream

/**
* Associate [sources] to an output file.
*
Expand All @@ -76,6 +108,17 @@ interface CodeGenerator {
*/
fun associate(sources: List<KSFile>, packageName: String, fileName: String, extensionName: String = "kt")

/**
* Associate [sources] to an output file.
*
* @param sources are [KSFile]s from which this output is built. Only those that are obtained directly
* from [Resolver] are required.
* @param path corresponds to the relative path of the generated file; includes the full file name
* @param fileType determines the target directory where the file should exist
* @see [CodeGenerator] for more details.
*/
fun associateByPath(sources: List<KSFile>, path: String, extensionName: String = "kt")

/**
* Associate [classes] to an output file.
*
Expand Down
Expand Up @@ -24,6 +24,7 @@ import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFile
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.OutputStream

class CodeGeneratorImpl(
Expand Down Expand Up @@ -54,13 +55,7 @@ class CodeGeneratorImpl(
fun pathOf(packageName: String, fileName: String, extensionName: String): String {
val packageDirs = if (packageName != "") "${packageName.split(".").joinToString(separator)}$separator" else ""
val extension = if (extensionName != "") ".$extensionName" else ""
val typeRoot = when (extensionName) {
"class" -> classDir
"java" -> javaDir
"kt" -> kotlinDir
else -> resourcesDir
}.path
return "$typeRoot$separator$packageDirs$fileName$extension"
return "$packageDirs$fileName$extension"
}

override fun createNewFile(
Expand All @@ -69,17 +64,64 @@ class CodeGeneratorImpl(
fileName: String,
extensionName: String
): OutputStream {
return createNewFile(
dependencies,
pathOf(packageName, fileName, extensionName),
extensionToDirectory(extensionName)
)
}

override fun createNewFileByPath(dependencies: Dependencies, path: String, extensionName: String): OutputStream {
val extension = if (extensionName != "") ".$extensionName" else ""
return createNewFile(dependencies, path + extension, extensionToDirectory(extensionName))
}

override fun associate(sources: List<KSFile>, packageName: String, fileName: String, extensionName: String) {
associate(sources, pathOf(packageName, fileName, extensionName), extensionToDirectory(extensionName))
}

override fun associateByPath(sources: List<KSFile>, path: String, extensionName: String) {
val extension = if (extensionName != "") ".$extensionName" else ""
associate(sources, path + extension, extensionToDirectory(extensionName))
}

override fun associateWithClasses(
classes: List<KSClassDeclaration>,
packageName: String,
fileName: String,
extensionName: String
) {
val path = pathOf(packageName, fileName, extensionName)
val file = File(path)
if (path in fileMap) {
val files = classes.map {
it.containingFile ?: NoSourceFile(projectBase, it.qualifiedName?.asString().toString())
}
associate(files, path, extensionToDirectory(extensionName))
}

private fun extensionToDirectory(extensionName: String): File {
return when (extensionName) {
"class" -> classDir
"java" -> javaDir
"kt" -> kotlinDir
else -> resourcesDir
}
}

private fun createNewFile(dependencies: Dependencies, path: String, baseDir: File): OutputStream {
val file = File(baseDir, path)
if (!isWithinBaseDir(baseDir, file)) {
throw IllegalStateException("requested path is outside the bounds of the required directory")
}
val absolutePath = file.absolutePath
if (absolutePath in fileMap) {
throw FileAlreadyExistsException(file)
}
val parentFile = file.parentFile
if (!parentFile.exists() && !parentFile.mkdirs()) {
throw IllegalStateException("failed to make parent directories.")
}
file.writeText("")
fileMap[path] = file
fileMap[absolutePath] = file
val sources = if (dependencies.isAllSources) {
allSources + anyChangesWildcard
} else {
Expand All @@ -89,41 +131,41 @@ class CodeGeneratorImpl(
dependencies.originatingFiles
}
}
associate(sources, path)
fileOutputStreamMap[path] = fileMap[path]!!.outputStream()
return fileOutputStreamMap[path]!!
associate(sources, file)
fileOutputStreamMap[absolutePath] = fileMap[absolutePath]!!.outputStream()
return fileOutputStreamMap[absolutePath]!!
}

override fun associate(sources: List<KSFile>, packageName: String, fileName: String, extensionName: String) {
val path = pathOf(packageName, fileName, extensionName)
associate(sources, path)
private fun isWithinBaseDir(baseDir: File, file: File): Boolean {
val base = baseDir.toPath().normalize()
return try {
val relativePath = file.toPath().normalize()
relativePath.startsWith(base)
} catch (e: IOException) {
false
}
}

override fun associateWithClasses(
classes: List<KSClassDeclaration>,
packageName: String,
fileName: String,
extensionName: String
) {
val path = pathOf(packageName, fileName, extensionName)
val files = classes.map {
it.containingFile ?: NoSourceFile(projectBase, it.qualifiedName?.asString().toString())
private fun associate(sources: List<KSFile>, path: String, baseDir: File) {
val file = File(baseDir, path)
if (!isWithinBaseDir(baseDir, file)) {
throw IllegalStateException("requested path is outside the bounds of the required directory")
}
associate(files, path)
associate(sources, file)
}

private fun associate(sources: List<KSFile>, outputPath: String) {
private fun associate(sources: List<KSFile>, outputPath: File) {
if (!isIncremental)
return

val output = File(outputPath).relativeTo(projectBase)
val output = outputPath.relativeTo(projectBase)
sources.forEach { source ->
sourceToOutputs.getOrPut(File(source.filePath).relativeTo(projectBase)) { mutableSetOf() }.add(output)
}
}

val outputs: Set<File>
get() = fileMap.keys.mapTo(mutableSetOf()) { File(it).relativeTo(projectBase) }
get() = fileMap.values.mapTo(mutableSetOf()) { it.relativeTo(projectBase) }

override val generatedFile: Collection<File>
get() = fileOutputStreamMap.keys.map { fileMap[it]!! }
Expand Down
@@ -0,0 +1,128 @@
package com.google.devtools.ksp.processing.impl

import com.google.devtools.ksp.AnyChanges
import com.google.devtools.ksp.processing.Dependencies
import org.junit.Assert
import org.junit.Before
import org.junit.Test
import java.io.File
import java.nio.file.Files

class CodeGeneratorImplTest {

lateinit var codeGenerator: CodeGeneratorImpl
lateinit var baseDir: File

@Before
fun setup() {
baseDir = Files.createTempDirectory("project").toFile()
val classesDir = File(baseDir, "classes")
classesDir.mkdir()
val javaDir = File(baseDir, "java")
javaDir.mkdir()
val kotlinDir = File(baseDir, "kotlin")
kotlinDir.mkdir()
val resourcesDir = File(baseDir, "resources")
resourcesDir.mkdir()
codeGenerator = CodeGeneratorImpl(
classesDir,
javaDir,
kotlinDir,
resourcesDir,
baseDir,
AnyChanges(baseDir),
emptyList(),
true
)
}

@Test
fun testCreatingAFile() {
codeGenerator.createNewFile(Dependencies.ALL_FILES, "a.b.c", "Test", "java")
codeGenerator.createNewFile(Dependencies.ALL_FILES, "a.b.c", "Test", "kt")
codeGenerator.createNewFile(Dependencies.ALL_FILES, "a.b.c", "Test", "class")
codeGenerator.createNewFile(Dependencies.ALL_FILES, "a.b.c", "Test", "")

val files = codeGenerator.generatedFile.toList()
Assert.assertEquals(File(baseDir, "java/a/b/c/Test.java"), files[0])
Assert.assertEquals(File(baseDir, "kotlin/a/b/c/Test.kt"), files[1])
Assert.assertEquals(File(baseDir, "classes/a/b/c/Test.class"), files[2])
Assert.assertEquals(File(baseDir, "resources/a/b/c/Test"), files[3])

try {
codeGenerator.outputs
} catch (e: Exception) {
Assert.fail("Failed to get outputs: ${e.message}")
}
}

@Test
fun testCreatingAFileWithSlash() {
codeGenerator.createNewFile(Dependencies.ALL_FILES, "a/b/c", "Test", "java")
codeGenerator.createNewFile(Dependencies.ALL_FILES, "a/b/c", "Test", "kt")
codeGenerator.createNewFile(Dependencies.ALL_FILES, "a/b/c", "Test", "class")
codeGenerator.createNewFile(Dependencies.ALL_FILES, "a/b/c", "Test", "")

val files = codeGenerator.generatedFile.toList()
Assert.assertEquals(File(baseDir, "java/a/b/c/Test.java"), files[0])
Assert.assertEquals(File(baseDir, "kotlin/a/b/c/Test.kt"), files[1])
Assert.assertEquals(File(baseDir, "classes/a/b/c/Test.class"), files[2])
Assert.assertEquals(File(baseDir, "resources/a/b/c/Test"), files[3])

try {
codeGenerator.outputs
} catch (e: Exception) {
Assert.fail("Failed to get outputs: ${e.message}")
}
}

@Test
fun testCreatingAFileWithPath() {
codeGenerator.createNewFileByPath(Dependencies.ALL_FILES, "a/b/c/Test", "java")
codeGenerator.createNewFileByPath(Dependencies.ALL_FILES, "a/b/c/Test")
codeGenerator.createNewFileByPath(Dependencies.ALL_FILES, "a/b/c/Test", "class")
codeGenerator.createNewFileByPath(Dependencies.ALL_FILES, "a/b/c/Test", "")

val files = codeGenerator.generatedFile.toList()
Assert.assertEquals(File(baseDir, "java/a/b/c/Test.java"), files[0])
Assert.assertEquals(File(baseDir, "kotlin/a/b/c/Test.kt"), files[1])
Assert.assertEquals(File(baseDir, "classes/a/b/c/Test.class"), files[2])
Assert.assertEquals(File(baseDir, "resources/a/b/c/Test"), files[3])

try {
codeGenerator.outputs
} catch (e: Exception) {
Assert.fail("Failed to get outputs: ${e.message}")
}
}

@Test
fun testCreatingAFileWithPathAndDots() {
codeGenerator.createNewFileByPath(Dependencies.ALL_FILES, "a/b/c/dir.with.dot/Test", "java")
codeGenerator.createNewFileByPath(Dependencies.ALL_FILES, "a/b/c/dir.with.dot/Test")
codeGenerator.createNewFileByPath(Dependencies.ALL_FILES, "a/b/c/dir.with.dot/Test", "class")
codeGenerator.createNewFileByPath(Dependencies.ALL_FILES, "a/b/c/dir.with.dot/Test", "")

val files = codeGenerator.generatedFile.toList()
Assert.assertEquals(File(baseDir, "java/a/b/c/dir.with.dot/Test.java"), files[0])
Assert.assertEquals(File(baseDir, "kotlin/a/b/c/dir.with.dot/Test.kt"), files[1])
Assert.assertEquals(File(baseDir, "classes/a/b/c/dir.with.dot/Test.class"), files[2])
Assert.assertEquals(File(baseDir, "resources/a/b/c/dir.with.dot/Test"), files[3])

try {
codeGenerator.outputs
} catch (e: Exception) {
Assert.fail("Failed to get outputs: ${e.message}")
}
}

@Test
fun testCreatingAFileByPathWithInvalidPath() {
try {
codeGenerator.createNewFileByPath(Dependencies.ALL_FILES, "../../b/c/Test", "java")
Assert.fail()
} catch (e: java.lang.IllegalStateException) {
Assert.assertEquals(e.message, "requested path is outside the bounds of the required directory")
}
}
}

0 comments on commit 857e994

Please sign in to comment.