• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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