Skip to content

Commit

Permalink
Handle errors and call SymbolProcessor.onError()
Browse files Browse the repository at this point in the history
  • Loading branch information
ting-yuan committed Aug 20, 2023
1 parent e9352b4 commit e43fde1
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 105 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.processing.impl.CodeGeneratorImpl
import com.google.devtools.ksp.processing.impl.JvmPlatformInfoImpl
import com.google.devtools.ksp.symbol.KSFile
import com.google.devtools.ksp.symbol.KSNode
import com.google.devtools.ksp.symbol.Origin
import com.google.devtools.ksp.toKotlinVersion
import com.intellij.core.CoreApplicationEnvironment
Expand Down Expand Up @@ -118,6 +119,15 @@ import java.nio.file.Paths
class KotlinSymbolProcessing(
val kspConfig: KSPJvmConfig,
) {
enum class ExitCode(code: Int) {
OK(0),

// Whenever there are some error messages.
PROCESSING_ERROR(1),

// Let exceptions pop through to the caller. Don't catch and convert them to, e.g., INTERNAL_ERROR.
}

init {
// We depend on swing (indirectly through PSI or something), so we want to declare headless mode,
// to avoid accidentally starting the UI thread. But, don't set it if it was set externally.
Expand Down Expand Up @@ -327,8 +337,53 @@ class KotlinSymbolProcessing(
}
}

private fun prepareAllKSFiles(
kotlinCoreProjectEnvironment: KotlinCoreProjectEnvironment,
modules: List<KtModule>,
compilerConfiguration: CompilerConfiguration
): List<KSFile> {
val project = kotlinCoreProjectEnvironment.project
val psiManager = PsiManager.getInstance(project)
val ktFiles = createSourceFilesFromSourceRoots(
compilerConfiguration, project, compilerConfiguration.kotlinSourceRoots
).toSet().toList()
val psiFiles = getPsiFilesFromPaths<PsiFileSystemItem>(
project,
getSourceFilePaths(compilerConfiguration, includeDirectoryRoot = true)
)

// Update Kotlin providers for newly generated source files.
(
project.getService(
KotlinDeclarationProviderFactory::class.java
) as IncrementalKotlinDeclarationProviderFactory
).update(ktFiles)
(
project.getService(
KotlinPackageProviderFactory::class.java
) as IncrementalKotlinPackageProviderFactory
).update(ktFiles)

// Update Java providers for newly generated source files.
reinitJavaFileManager(kotlinCoreProjectEnvironment, modules, psiFiles)

val localFileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL)
val javaRoots = kspConfig.javaSourceRoots + kspConfig.javaOutputDir
// Get non-symbolic paths first
val javaFiles = javaRoots.sortedBy { Files.isSymbolicLink(it.toPath()) }
.flatMap { root -> root.walk().filter { it.isFile && it.extension == "java" }.toList() }
// This time is for .java files
.sortedBy { Files.isSymbolicLink(it.toPath()) }
.distinctBy { it.canonicalPath }
.mapNotNull { localFileSystem.findFileByPath(it.path)?.let { psiManager.findFile(it) } as? PsiJavaFile }

return ktFiles.map { analyze { KSFileImpl.getCached(it.getFileSymbol()) } } +
javaFiles.map { KSFileJavaImpl.getCached(it) }
}

// TODO: performance
@OptIn(KtAnalysisApiInternals::class)
fun execute() {
fun execute(): ExitCode {
// TODO: CompilerConfiguration is deprecated.
val compilerConfiguration: CompilerConfiguration = CompilerConfiguration().apply {
addKotlinSourceRoots(kspConfig.sourceRoots.map { it.path })
Expand All @@ -349,108 +404,68 @@ class KotlinSymbolProcessing(
}

val (analysisAPISession, kotlinCoreProjectEnvironment, modules) = createAASession(compilerConfiguration)
val project = analysisAPISession.project
val kspCoreEnvironment = KSPCoreEnvironment(project as MockProject)

val kspCoreEnvironment = KSPCoreEnvironment(analysisAPISession.project as MockProject)
val logger = object : KSPLogger by kspConfig.logger {
var hasError: Boolean = false

override fun error(message: String, symbol: KSNode?) {
hasError = true
kspConfig.logger.error(message, symbol)
}
}

// TODO: error handling, onError()
// TODO: performance
val project = analysisAPISession.project
val psiManager = PsiManager.getInstance(project)
val providers: List<SymbolProcessorProvider> = kspConfig.processorProviders
ResolverAAImpl.ktModule = modules.single()

// Initializing environments
var allKSFiles = prepareAllKSFiles(kotlinCoreProjectEnvironment, modules, compilerConfiguration)
var newKSFiles = allKSFiles
val anyChangesWildcard = AnyChanges(kspConfig.projectBaseDir)
val codeGenerator = CodeGeneratorImpl(
kspConfig.classOutputDir,
{ kspConfig.javaOutputDir },
kspConfig.kotlinOutputDir,
kspConfig.resourceOutputDir,
kspConfig.projectBaseDir,
anyChangesWildcard,
allKSFiles,
kspConfig.incremental
)

val deferredSymbols = mutableMapOf<SymbolProcessor, List<Restorable>>()
var finished = false
var initialized = false
lateinit var codeGenerator: CodeGeneratorImpl
lateinit var processors: List<SymbolProcessor>
lateinit var newKSFiles: List<KSFile>
lateinit var newFileNames: Set<String>
while (!finished) {
val ktFiles = createSourceFilesFromSourceRoots(
compilerConfiguration, analysisAPISession.project, compilerConfiguration.kotlinSourceRoots
).toSet().toList()
val psiFiles = getPsiFilesFromPaths<PsiFileSystemItem>(
project,
getSourceFilePaths(compilerConfiguration, includeDirectoryRoot = true)
)

// Update Kotlin providers for newly generated source files.
(
project.getService(
KotlinDeclarationProviderFactory::class.java
) as IncrementalKotlinDeclarationProviderFactory
).update(ktFiles)
(
project.getService(
KotlinPackageProviderFactory::class.java
) as IncrementalKotlinPackageProviderFactory
).update(ktFiles)

// Update Java providers for newly generated source files.
reinitJavaFileManager(kotlinCoreProjectEnvironment, modules, psiFiles)

ResolverAAImpl.ktModule = modules.single()
ResolverAAImpl.functionAsMemberOfCache = mutableMapOf()
ResolverAAImpl.propertyAsMemberOfCache = mutableMapOf()
val localFileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL)
val javaRoots = kspConfig.javaSourceRoots + kspConfig.javaOutputDir
// Get non-symbolic paths first
val javaFiles = javaRoots.sortedBy { Files.isSymbolicLink(it.toPath()) }
.flatMap { root -> root.walk().filter { it.isFile && it.extension == "java" }.toList() }
// This time is for .java files
.sortedBy { Files.isSymbolicLink(it.toPath()) }
.distinctBy { it.canonicalPath }
.mapNotNull { localFileSystem.findFileByPath(it.path)?.let { psiManager.findFile(it) } as? PsiJavaFile }

val allKSFiles =
ktFiles.map { analyze { KSFileImpl.getCached(it.getFileSymbol()) } } +
javaFiles.map { KSFileJavaImpl.getCached(it) }
if (!initialized) {
val anyChangesWildcard = AnyChanges(kspConfig.projectBaseDir)
codeGenerator = CodeGeneratorImpl(
kspConfig.classOutputDir,
{ kspConfig.javaOutputDir },
kspConfig.kotlinOutputDir,
kspConfig.resourceOutputDir,
kspConfig.projectBaseDir,
anyChangesWildcard,
allKSFiles,
kspConfig.incremental
)

processors = providers.mapNotNull { provider ->
var processor: SymbolProcessor? = null
processor = provider.create(
SymbolProcessorEnvironment(
kspConfig.processorOptions,
kspConfig.languageVersion.toKotlinVersion(),
codeGenerator,
kspConfig.logger,
kspConfig.apiVersion.toKotlinVersion(),
// TODO: compilerVersion
KotlinVersion.CURRENT,
// TODO: fix platform info
listOf(JvmPlatformInfoImpl("JVM", "1.8", "disable"))
)
)
processor.also { deferredSymbols[it] = mutableListOf() }
}
val symbolProcessorEnvironment = SymbolProcessorEnvironment(
kspConfig.processorOptions,
kspConfig.languageVersion.toKotlinVersion(),
codeGenerator,
logger,
kspConfig.apiVersion.toKotlinVersion(),
// TODO: compilerVersion
KotlinVersion.CURRENT,
// TODO: fix platform info
listOf(JvmPlatformInfoImpl("JVM", "1.8", "disable"))
)

newKSFiles = allKSFiles
// Load and instantiate processsors
val deferredSymbols = mutableMapOf<SymbolProcessor, List<Restorable>>()
val processors = providers.map { provider ->
provider.create(symbolProcessorEnvironment).also { deferredSymbols[it] = mutableListOf() }
}

initialized = true
} else {
newKSFiles = allKSFiles.filter {
it.filePath in newFileNames
}
}
// Run processors until either
// 1) there is an error
// 2) there is no more new files.
while (!logger.hasError) {
val resolver = ResolverAAImpl(
allKSFiles,
newKSFiles,
deferredSymbols,
analysisAPISession.project
project
)
ResolverAAImpl.instance = resolver
ResolverAAImpl.functionAsMemberOfCache = mutableMapOf()
ResolverAAImpl.propertyAsMemberOfCache = mutableMapOf()

processors.forEach {
deferredSymbols[it] =
Expand All @@ -461,24 +476,35 @@ class KotlinSymbolProcessing(
}
}

if (codeGenerator.generatedFile.isEmpty()) {
finished = true
processors.forEach { it.finish() }
} else {
// Drop caches
KotlinGlobalModificationService.getInstance(project).publishGlobalModuleStateModification()
KtAnalysisSessionProvider.getInstance(project).clearCaches()
psiManager.dropResolveCaches()
psiManager.dropPsiCaches()
if (logger.hasError || codeGenerator.generatedFile.isEmpty()) {
break
}

KSObjectCacheManager.clear()
// Drop caches
KotlinGlobalModificationService.getInstance(project).publishGlobalModuleStateModification()
KtAnalysisSessionProvider.getInstance(project).clearCaches()
psiManager.dropResolveCaches()
psiManager.dropPsiCaches()

newFileNames = codeGenerator.generatedFile.filter { it.extension == "kt" || it.extension == "java" }
.map { it.canonicalPath }.toSet()
}
KSObjectCacheManager.clear()

val newFilePaths = codeGenerator.generatedFile.filter { it.extension == "kt" || it.extension == "java" }
.map { it.canonicalPath }.toSet()
allKSFiles = prepareAllKSFiles(kotlinCoreProjectEnvironment, modules, compilerConfiguration)
newKSFiles = allKSFiles.filter { it.filePath in newFilePaths }
codeGenerator.closeFiles()
}

// Call onError() or finish()
if (logger.hasError) {
processors.forEach(SymbolProcessor::onError)
} else {
processors.forEach(SymbolProcessor::finish)
}

codeGenerator.closeFiles()

return if (logger.hasError) ExitCode.PROCESSING_ERROR else ExitCode.OK
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,10 @@ abstract class AbstractKSPAATest : AbstractKSPTest(FrontendKinds.FIR) {
cachesDir = File(testRoot, "kspTest/kspCaches")
outputBaseDir = File(testRoot, "kspTest")
}.build()
val ksp = KotlinSymbolProcessing(kspConfig)
ksp.execute()
val exitCode = KotlinSymbolProcessing(kspConfig).execute()
if (exitCode != KotlinSymbolProcessing.ExitCode.OK) {
return listOf("KSP FAILED WITH EXIT CODE: ${exitCode.name}") + testProcessor.toResult()
}
return testProcessor.toResult()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -659,4 +659,10 @@ class KSPAATest : AbstractKSPAATest() {
fun testDeferredTypeRefs() {
runTest("../test-utils/testData/api/deferredTypeRefs.kt")
}

@TestMetadata("exitCode.kt")
@Test
fun testExitCode() {
runTest("../test-utils/testData/api/exitCode.kt")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.google.devtools.ksp.processor

import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.processing.SymbolProcessor
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.symbol.KSAnnotated

class ExitCodeProcessor : AbstractTestProcessor() {
override fun toResult(): List<String> = emptyList()

override fun process(resolver: Resolver): List<KSAnnotated> {
if (resolver.getNewFiles().single().fileName == "PrintError.kt") {
env.logger.error("An error")
}

return emptyList()
}

lateinit var env: SymbolProcessorEnvironment

override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
env = environment
return this
}
}
24 changes: 24 additions & 0 deletions test-utils/testData/api/exitCode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2022 Google LLC
* Copyright 2010-2022 JetBrains s.r.o. and Kotlin Programming Language contributors.
*
* 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.
*/

// TEST PROCESSOR: ExitCodeProcessor
// EXPECTED:
// KSP FAILED WITH EXIT CODE: PROCESSING_ERROR
// END

// FILE: PrintError.kt
class PrintError

0 comments on commit e43fde1

Please sign in to comment.