1 /*
<lambda>null2 * Copyright 2020 Google LLC
3 * Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package com.google.devtools.ksp
19
20 import com.google.devtools.ksp.processing.KSPLogger
21 import com.google.devtools.ksp.processing.PlatformInfo
22 import com.google.devtools.ksp.processing.SymbolProcessor
23 import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
24 import com.google.devtools.ksp.processing.SymbolProcessorProvider
25 import com.google.devtools.ksp.processing.impl.CodeGeneratorImpl
26 import com.google.devtools.ksp.processing.impl.JsPlatformInfoImpl
27 import com.google.devtools.ksp.processing.impl.JvmPlatformInfoImpl
28 import com.google.devtools.ksp.processing.impl.KSPCompilationError
29 import com.google.devtools.ksp.processing.impl.MessageCollectorBasedKSPLogger
30 import com.google.devtools.ksp.processing.impl.NativePlatformInfoImpl
31 import com.google.devtools.ksp.processing.impl.ResolverImpl
32 import com.google.devtools.ksp.processing.impl.UnknownPlatformInfoImpl
33 import com.google.devtools.ksp.symbol.KSAnnotated
34 import com.google.devtools.ksp.symbol.KSClassDeclaration
35 import com.google.devtools.ksp.symbol.KSDeclarationContainer
36 import com.google.devtools.ksp.symbol.KSFile
37 import com.google.devtools.ksp.symbol.KSPropertyDeclaration
38 import com.google.devtools.ksp.symbol.KSVisitorVoid
39 import com.google.devtools.ksp.symbol.Modifier
40 import com.google.devtools.ksp.symbol.Origin
41 import com.google.devtools.ksp.symbol.Visibility
42 import com.google.devtools.ksp.symbol.impl.java.KSFileJavaImpl
43 import com.google.devtools.ksp.symbol.impl.kotlin.KSFileImpl
44 import com.intellij.openapi.project.Project
45 import com.intellij.openapi.vfs.StandardFileSystems
46 import com.intellij.openapi.vfs.VirtualFileManager
47 import com.intellij.psi.PsiJavaFile
48 import com.intellij.psi.PsiManager
49 import org.jetbrains.kotlin.analyzer.AnalysisResult
50 import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity
51 import org.jetbrains.kotlin.container.ComponentProvider
52 import org.jetbrains.kotlin.context.ProjectContext
53 import org.jetbrains.kotlin.descriptors.ModuleDescriptor
54 import org.jetbrains.kotlin.load.java.components.FilesByFacadeFqNameIndexer
55 import org.jetbrains.kotlin.platform.js.JsPlatform
56 import org.jetbrains.kotlin.platform.jvm.JdkPlatform
57 import org.jetbrains.kotlin.platform.konan.NativePlatform
58 import org.jetbrains.kotlin.psi.KtFile
59 import org.jetbrains.kotlin.resolve.BindingContext
60 import org.jetbrains.kotlin.resolve.BindingTrace
61 import org.jetbrains.kotlin.resolve.extensions.AnalysisHandlerExtension
62 import org.jetbrains.kotlin.util.ServiceLoaderLite
63 import java.io.File
64 import java.io.PrintWriter
65 import java.io.StringWriter
66 import java.net.URLClassLoader
67 import java.nio.file.Files
68
69 class KotlinSymbolProcessingExtension(
70 options: KspOptions,
71 logger: KSPLogger,
72 val testProcessor: SymbolProcessorProvider? = null,
73 ) : AbstractKotlinSymbolProcessingExtension(options, logger, testProcessor != null) {
74 override fun loadProviders(): List<SymbolProcessorProvider> {
75 if (!initialized) {
76 providers = if (testProcessor != null) {
77 listOf(testProcessor)
78 } else {
79 val processingClasspath = options.processingClasspath
80 val classLoader =
81 URLClassLoader(processingClasspath.map { it.toURI().toURL() }.toTypedArray(), javaClass.classLoader)
82
83 ServiceLoaderLite.loadImplementations(SymbolProcessorProvider::class.java, classLoader).filter {
84 (options.processors.isEmpty() && it.javaClass.name !in options.excludedProcessors) ||
85 it.javaClass.name in options.processors
86 }
87 }
88 if (providers.isEmpty()) {
89 logger.error("No providers found in processor classpath.")
90 } else {
91 logger.info(
92 "loaded provider(s): " +
93 "${providers.joinToString(separator = ", ", prefix = "[", postfix = "]") { it.javaClass.name }}"
94 )
95 }
96 }
97 return providers
98 }
99 }
100
101 abstract class AbstractKotlinSymbolProcessingExtension(
102 val options: KspOptions,
103 val logger: KSPLogger,
104 val testMode: Boolean,
105 ) :
106 AnalysisHandlerExtension {
107 var initialized = false
108 var finished = false
109 val deferredSymbols = mutableMapOf<SymbolProcessor, List<KSAnnotated>>()
110 lateinit var providers: List<SymbolProcessorProvider>
111 lateinit var processors: List<SymbolProcessor>
112 lateinit var incrementalContext: IncrementalContext
113 lateinit var dirtyFiles: Set<KSFile>
114 lateinit var cleanFilenames: Set<String>
115 lateinit var codeGenerator: CodeGeneratorImpl
116 var newFileNames: Collection<String> = emptySet()
117 var rounds = 0
118
119 companion object {
120 private const val KSP_PACKAGE_NAME = "com.google.devtools.ksp"
121 private const val KOTLIN_PACKAGE_NAME = "org.jetbrains.kotlin"
122 private const val MULTI_ROUND_THRESHOLD = 100
123 }
124
doAnalysisnull125 override fun doAnalysis(
126 project: Project,
127 module: ModuleDescriptor,
128 projectContext: ProjectContext,
129 files: Collection<KtFile>,
130 bindingTrace: BindingTrace,
131 componentProvider: ComponentProvider,
132 ): AnalysisResult? {
133 // with `withCompilation == true`:
134 // * KSP returns AnalysisResult.RetryWithAdditionalRoots in last round of processing, to notify compiler the generated sources.
135 // * This function will be called again, and returning null tells compiler to fall through with normal compilation.
136 if (finished) {
137 if (!options.withCompilation)
138 throw IllegalStateException("KSP is re-entered unexpectedly.")
139 if (!options.returnOkOnError && logger.hasError()) {
140 return AnalysisResult.compilationError(BindingContext.EMPTY)
141 }
142 return null
143 }
144
145 rounds++
146 if (rounds > MULTI_ROUND_THRESHOLD) {
147 logger.warn("Current processing rounds exceeds 100, check processors for potential infinite rounds")
148 }
149 logger.logging("round $rounds of processing")
150 val psiManager = PsiManager.getInstance(project)
151 if (initialized) {
152 psiManager.dropPsiCaches()
153 }
154
155 val localFileSystem = VirtualFileManager.getInstance().getFileSystem(StandardFileSystems.FILE_PROTOCOL)
156 val javaSourceRoots =
157 if (initialized) options.javaSourceRoots + options.javaOutputDir else options.javaSourceRoots
158 val javaFiles = javaSourceRoots
159 .sortedBy { Files.isSymbolicLink(it.toPath()) } // Get non-symbolic paths first
160 .flatMap { root -> root.walk().filter { it.isFile && it.extension == "java" }.toList() }
161 .sortedBy { Files.isSymbolicLink(it.toPath()) } // This time is for .java files
162 .distinctBy { it.canonicalPath }
163 .mapNotNull { localFileSystem.findFileByPath(it.path)?.let { psiManager.findFile(it) } as? PsiJavaFile }
164
165 val anyChangesWildcard = AnyChanges(options.projectBaseDir)
166 val commonSources: Set<String?> = options.commonSources.map { it.canonicalPath }.toSet()
167 val ksFiles = files.filterNot { it.virtualFile.canonicalPath in commonSources }
168 .map { KSFileImpl.getCached(it) } + javaFiles.map { KSFileJavaImpl.getCached(it) }
169 lateinit var newFiles: List<KSFile>
170
171 handleException(module, project) {
172 val fileClassProcessor = FilesByFacadeFqNameIndexer(bindingTrace)
173 if (!initialized) {
174 incrementalContext = IncrementalContext(
175 options, componentProvider,
176 File(anyChangesWildcard.filePath).relativeTo(options.projectBaseDir)
177 )
178 dirtyFiles = incrementalContext.calcDirtyFiles(ksFiles).toSet()
179 cleanFilenames = ksFiles.filterNot { it in dirtyFiles }.map { it.filePath }.toSet()
180 newFiles = dirtyFiles.toList()
181 // DO NOT filter out common sources.
182 files.forEach { fileClassProcessor.preprocessFile(it) }
183 } else {
184 newFiles = ksFiles.filter {
185 when (it) {
186 is KSFileImpl -> it.file
187 is KSFileJavaImpl -> it.psi
188 else -> null
189 }?.virtualFile?.let { virtualFile ->
190 if (System.getProperty("os.name").startsWith("windows", ignoreCase = true)) {
191 virtualFile.canonicalPath ?: virtualFile.path
192 } else {
193 File(virtualFile.path).canonicalPath
194 }
195 } in newFileNames
196 }
197 incrementalContext.registerGeneratedFiles(newFiles)
198 newFiles.filterIsInstance<KSFileImpl>().forEach { fileClassProcessor.preprocessFile(it.file) }
199 }
200 }?.let { return@doAnalysis it }
201
202 // dirtyFiles cannot be reused because they are created in the old container.
203 val resolver = ResolverImpl(
204 module,
205 ksFiles.filterNot {
206 it.filePath in cleanFilenames
207 },
208 newFiles, deferredSymbols, bindingTrace, project, componentProvider, incrementalContext, options
209 )
210
211 if (!initialized) {
212 // Visit constants so that JavaPsiFacade knows them.
213 // The annotation visitor in ResolverImpl covered newFiles already.
214 // Skip private and local members, which are not visible to Java files.
215 ksFiles.filterIsInstance<KSFileImpl>().filter { it !in dirtyFiles }.forEach {
216 try {
217 it.accept(
218 object : KSVisitorVoid() {
219 private fun visitDeclarationContainer(container: KSDeclarationContainer) {
220 container.declarations.filterNot {
221 it.getVisibility() == Visibility.PRIVATE
222 }.forEach {
223 it.accept(this, Unit)
224 }
225 }
226
227 override fun visitFile(file: KSFile, data: Unit) =
228 visitDeclarationContainer(file)
229
230 override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) =
231 visitDeclarationContainer(classDeclaration)
232
233 override fun visitPropertyDeclaration(property: KSPropertyDeclaration, data: Unit) {
234 if (property.modifiers.contains(Modifier.CONST)) {
235 property.getter // force resolution
236 }
237 }
238 },
239 Unit
240 )
241 } catch (_: Exception) {
242 // Do nothing.
243 }
244 }
245 }
246
247 val providers = loadProviders()
248 if (!initialized) {
249 codeGenerator = CodeGeneratorImpl(
250 options.classOutputDir,
251 options.javaOutputDir,
252 options.kotlinOutputDir,
253 options.resourceOutputDir,
254 options.projectBaseDir,
255 anyChangesWildcard,
256 ksFiles,
257 options.incremental
258 )
259 processors = providers.mapNotNull { provider ->
260 var processor: SymbolProcessor? = null
261 handleException(module, project) {
262 processor = provider.create(
263 SymbolProcessorEnvironment(
264 options.processingOptions,
265 options.languageVersion,
266 codeGenerator,
267 logger,
268 options.apiVersion,
269 options.compilerVersion,
270 findTargetInfos(module)
271 )
272 )
273 }?.let { analysisResult ->
274 resolver.tearDown()
275 return@doAnalysis analysisResult
276 }
277 if (logger.hasError()) {
278 return@mapNotNull null
279 }
280 processor?.also { deferredSymbols[it] = mutableListOf() }
281 }
282 /* Kotlin compiler expects a source dir to exist, but processors might not generate Kotlin source.
283 Create it during initialization just in case. */
284 options.kotlinOutputDir.mkdirs()
285 initialized = true
286 }
287 if (!logger.hasError()) {
288 processors.forEach processing@{ processor ->
289 handleException(module, project) {
290 deferredSymbols[processor] =
291 processor.process(resolver).filter { it.origin == Origin.KOTLIN || it.origin == Origin.JAVA }
292 }?.let {
293 resolver.tearDown()
294 return it
295 }
296 if (logger.hasError()) {
297 return@processing
298 }
299 if (!deferredSymbols.containsKey(processor) || deferredSymbols[processor]!!.isEmpty()) {
300 deferredSymbols.remove(processor)
301 }
302 }
303 }
304 // Post processing.
305 newFileNames = codeGenerator.generatedFile.filter { it.extension == "kt" || it.extension == "java" }
306 .map { it.canonicalPath.replace(File.separatorChar, '/') }.toSet()
307 if (codeGenerator.generatedFile.isEmpty()) {
308 finished = true
309 }
310 KSObjectCacheManager.clear()
311 codeGenerator.closeFiles()
312 if (logger.hasError()) {
313 finished = true
314 processors.forEach { processor ->
315 handleException(module, project) {
316 processor.onError()
317 }?.let {
318 resolver.tearDown()
319 return it
320 }
321 }
322 } else {
323 if (finished) {
324 processors.forEach { processor ->
325 handleException(module, project) {
326 processor.finish()
327 }?.let {
328 resolver.tearDown()
329 return it
330 }
331 }
332 if (deferredSymbols.isNotEmpty()) {
333 deferredSymbols.map { entry ->
334 logger.warn(
335 "Unable to process:${entry.key::class.qualifiedName}: ${
336 entry.value.map { it.toString() }.joinToString(";")
337 }"
338 )
339 }
340 }
341 if (!logger.hasError()) {
342 incrementalContext.updateCachesAndOutputs(
343 dirtyFiles,
344 codeGenerator.outputs,
345 codeGenerator.sourceToOutputs
346 )
347 }
348 }
349 }
350 if (finished) {
351 logger.reportAll()
352 }
353 resolver.tearDown()
354 return if (finished && !options.withCompilation) {
355 if (!options.returnOkOnError && logger.hasError()) {
356 AnalysisResult.compilationError(BindingContext.EMPTY)
357 } else {
358 AnalysisResult.success(BindingContext.EMPTY, module, shouldGenerateCode = false)
359 }
360 } else {
361 AnalysisResult.RetryWithAdditionalRoots(
362 BindingContext.EMPTY,
363 module,
364 listOf(options.javaOutputDir),
365 listOf(options.kotlinOutputDir),
366 listOf(options.classOutputDir)
367 )
368 }
369 }
370
loadProvidersnull371 abstract fun loadProviders(): List<SymbolProcessorProvider>
372
373 private var annotationProcessingComplete = false
374
375 private fun setAnnotationProcessingComplete(): Boolean {
376 if (annotationProcessingComplete) return true
377
378 annotationProcessingComplete = true
379 return false
380 }
381
KSPLoggernull382 private fun KSPLogger.reportAll() {
383 (this as MessageCollectorBasedKSPLogger).reportAll()
384 }
385
KSPLoggernull386 private fun KSPLogger.hasError(): Boolean {
387 return (this as MessageCollectorBasedKSPLogger).recordedEvents.any {
388 it.severity == CompilerMessageSeverity.ERROR || it.severity == CompilerMessageSeverity.EXCEPTION
389 }
390 }
391
handleExceptionnull392 private fun handleException(module: ModuleDescriptor, project: Project, call: () -> Unit): AnalysisResult? {
393 try {
394 call()
395 } catch (e: Exception) {
396 fun Exception.logToError() {
397 val sw = StringWriter()
398 printStackTrace(PrintWriter(sw))
399 logger.error(sw.toString())
400 }
401
402 fun Exception.isNotRecoverable(): Boolean =
403 stackTrace.first().className.let {
404 // TODO: convert non-critical exceptions thrown by KSP to recoverable errors.
405 it.startsWith(KSP_PACKAGE_NAME) || it.startsWith(KOTLIN_PACKAGE_NAME)
406 }
407
408 // Returning non-null here allows
409 // 1. subsequent processing of other processors in current round.
410 // 2. processor.onError() be called.
411 //
412 // In other words, returning non-null let current round finish.
413 when {
414 e is KSPCompilationError -> {
415 logger.error("${project.findLocationString(e.file, e.offset)}: ${e.message}")
416 logger.reportAll()
417 return if (options.returnOkOnError) {
418 AnalysisResult.success(BindingContext.EMPTY, module, shouldGenerateCode = false)
419 } else {
420 AnalysisResult.compilationError(BindingContext.EMPTY)
421 }
422 }
423
424 e.isNotRecoverable() -> {
425 e.logToError()
426 logger.reportAll()
427 return if (options.returnOkOnError) {
428 AnalysisResult.success(BindingContext.EMPTY, module, shouldGenerateCode = false)
429 } else {
430 AnalysisResult.internalError(BindingContext.EMPTY, e)
431 }
432 }
433
434 // Let this round finish.
435 else -> {
436 e.logToError()
437 }
438 }
439 }
440 return null
441 }
442 }
443
findTargetInfosnull444 fun findTargetInfos(module: ModuleDescriptor): List<PlatformInfo> =
445 module.platform?.componentPlatforms?.map { platform ->
446 when (platform) {
447 is JdkPlatform -> JvmPlatformInfoImpl(platform.platformName, platform.targetVersion.toString())
448 is JsPlatform -> JsPlatformInfoImpl(platform.platformName)
449 is NativePlatform -> NativePlatformInfoImpl(platform.platformName, platform.targetName)
450 // Unknown platform; toString() may be more informative than platformName
451 else -> UnknownPlatformInfoImpl(platform.toString())
452 }
453 } ?: emptyList()
454