xref: /aosp_15_r20/external/ksp/compiler-plugin/src/main/kotlin/com/google/devtools/ksp/Incremental.kt (revision af87fb4bb8e3042070d2a054e912924f599b22b7)
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.symbol.*
21 import com.google.devtools.ksp.symbol.impl.findPsi
22 import com.google.devtools.ksp.symbol.impl.java.KSFunctionDeclarationJavaImpl
23 import com.google.devtools.ksp.symbol.impl.java.KSPropertyDeclarationJavaImpl
24 import com.google.devtools.ksp.visitor.KSDefaultVisitor
25 import com.intellij.psi.*
26 import com.intellij.psi.impl.source.PsiClassReferenceType
27 import com.intellij.util.containers.MultiMap
28 import com.intellij.util.io.DataExternalizer
29 import com.intellij.util.io.IOUtil
30 import com.intellij.util.io.KeyDescriptor
31 import org.jetbrains.kotlin.container.ComponentProvider
32 import org.jetbrains.kotlin.container.get
33 import org.jetbrains.kotlin.descriptors.CallableMemberDescriptor
34 import org.jetbrains.kotlin.descriptors.ClassDescriptor
35 import org.jetbrains.kotlin.incremental.*
36 import org.jetbrains.kotlin.incremental.components.LookupTracker
37 import org.jetbrains.kotlin.incremental.components.Position
38 import org.jetbrains.kotlin.incremental.components.ScopeKind
39 import org.jetbrains.kotlin.incremental.storage.BasicMap
40 import org.jetbrains.kotlin.incremental.storage.CollectionExternalizer
41 import org.jetbrains.kotlin.incremental.storage.FileToPathConverter
42 import org.jetbrains.kotlin.resolve.descriptorUtil.getAllSuperclassesWithoutAny
43 import org.jetbrains.kotlin.types.KotlinType
44 import org.jetbrains.kotlin.types.typeUtil.supertypes
45 import java.io.DataInput
46 import java.io.DataOutput
47 import java.io.File
48 import java.nio.file.Files
49 import java.nio.file.StandardCopyOption
50 import java.util.*
51 
52 abstract class PersistentMap<K : Comparable<K>, V>(
53     storageFile: File,
54     keyDescriptor: KeyDescriptor<K>,
55     valueExternalizer: DataExternalizer<V>,
56 ) : BasicMap<K, V>(storageFile, keyDescriptor, valueExternalizer) {
57     abstract operator fun get(key: K): V?
58     abstract operator fun set(key: K, value: V)
59     abstract fun remove(key: K)
60 }
61 
62 class FileToSymbolsMap(storageFile: File) : PersistentMap<File, Collection<LookupSymbol>>(
63     storageFile,
64     FileKeyDescriptor,
<lambda>null65     CollectionExternalizer(LookupSymbolExternalizer, { HashSet() })
66 ) {
dumpKeynull67     override fun dumpKey(key: File): String = key.toString()
68 
69     override fun dumpValue(value: Collection<LookupSymbol>): String = value.toString()
70 
71     fun add(file: File, symbol: LookupSymbol) {
72         storage.append(file, listOf(symbol))
73     }
74 
getnull75     override operator fun get(key: File): Collection<LookupSymbol>? = storage[key]
76 
77     override operator fun set(key: File, symbols: Collection<LookupSymbol>) {
78         storage[key] = symbols
79     }
80 
removenull81     override fun remove(key: File) {
82         storage.remove(key)
83     }
84 
85     val keys: Collection<File>
86         get() = storage.keys
87 }
88 
89 object FileKeyDescriptor : KeyDescriptor<File> {
readnull90     override fun read(input: DataInput): File {
91         return File(IOUtil.readString(input))
92     }
93 
savenull94     override fun save(output: DataOutput, value: File) {
95         IOUtil.writeString(value.path, output)
96     }
97 
getHashCodenull98     override fun getHashCode(value: File): Int = value.hashCode()
99 
100     override fun isEqual(val1: File, val2: File): Boolean = val1 == val2
101 }
102 
103 object LookupSymbolExternalizer : DataExternalizer<LookupSymbol> {
104     override fun read(input: DataInput): LookupSymbol = LookupSymbol(IOUtil.readString(input), IOUtil.readString(input))
105 
106     override fun save(output: DataOutput, value: LookupSymbol) {
107         IOUtil.writeString(value.name, output)
108         IOUtil.writeString(value.scope, output)
109     }
110 }
111 
112 object FileExternalizer : DataExternalizer<File> {
readnull113     override fun read(input: DataInput): File = File(IOUtil.readString(input))
114 
115     override fun save(output: DataOutput, value: File) {
116         IOUtil.writeString(value.path, output)
117     }
118 }
119 
120 class FileToFilesMap(storageFile: File) : PersistentMap<File, Collection<File>>(
121     storageFile,
122     FileKeyDescriptor,
<lambda>null123     CollectionExternalizer(FileExternalizer, { HashSet() })
124 ) {
125 
getnull126     override operator fun get(key: File): Collection<File>? = storage[key]
127 
128     override operator fun set(key: File, value: Collection<File>) {
129         storage[key] = value
130     }
131 
dumpKeynull132     override fun dumpKey(key: File): String = key.path
133 
134     override fun dumpValue(value: Collection<File>) =
135         value.dumpCollection()
136 
137     override fun remove(key: File) {
138         storage.remove(key)
139     }
140 
141     val keys: Collection<File>
142         get() = storage.keys
143 }
144 
145 object symbolCollector : KSDefaultVisitor<(LookupSymbol) -> Unit, Unit>() {
defaultHandlernull146     override fun defaultHandler(node: KSNode, data: (LookupSymbol) -> Unit) = Unit
147 
148     override fun visitDeclaration(declaration: KSDeclaration, data: (LookupSymbol) -> Unit) {
149         if (declaration.isPrivate())
150             return
151 
152         val name = declaration.simpleName.asString()
153         val scope =
154             declaration.qualifiedName?.asString()?.let { it.substring(0, Math.max(it.length - name.length - 1, 0)) }
155                 ?: return
156         data(LookupSymbol(name, scope))
157     }
158 
visitDeclarationContainernull159     override fun visitDeclarationContainer(declarationContainer: KSDeclarationContainer, data: (LookupSymbol) -> Unit) {
160         // Local declarations aren't visible to other files / classes.
161         if (declarationContainer is KSFunctionDeclaration)
162             return
163 
164         declarationContainer.declarations.forEach {
165             it.accept(this, data)
166         }
167     }
168 }
169 
170 internal class RelativeFileToPathConverter(val baseDir: File) : FileToPathConverter {
toPathnull171     override fun toPath(file: File): String = file.path
172     override fun toFile(path: String): File = File(path).relativeTo(baseDir)
173 }
174 
175 class IncrementalContext(
176     private val options: KspOptions,
177     private val componentProvider: ComponentProvider,
178     private val anyChangesWildcard: File,
179 ) {
180     // Symbols defined in changed files. This is used to update symbolsMap in the end.
181     private val updatedSymbols = MultiMap.createSet<File, LookupSymbol>()
182 
183     // Sealed classes / interfaces on which `getSealedSubclasses` is invoked.
184     // This is used to update sealedMap in the end.
185     private val updatedSealed = MultiMap.createSet<File, LookupSymbol>()
186 
187     // Sealed classes / interfaces on which `getSealedSubclasses` is invoked.
188     // This is saved across processing.
189     private val sealedMap = FileToSymbolsMap(File(options.cachesDir, "sealed"))
190 
191     // Symbols defined in each file. This is saved across processing.
192     private val symbolsMap = FileToSymbolsMap(File(options.cachesDir, "symbols"))
193 
194     private val cachesUpToDateFile = File(options.cachesDir, "caches.uptodate")
195     private val rebuild = !cachesUpToDateFile.exists()
196 
197     private val baseDir = options.projectBaseDir
198 
199     private val logsDir = File(options.cachesDir, "logs").apply { mkdirs() }
200     private val buildTime = Date().time
201 
202     private val modified = options.knownModified.map { it.relativeTo(baseDir) }.toSet()
203     private val removed = options.knownRemoved.map { it.relativeTo(baseDir) }.toSet()
204 
205     private val lookupTracker: LookupTracker = componentProvider.get()
206 
207     // Disable incremental processing if somehow DualLookupTracker failed to be registered.
208     // This may happen when a platform hasn't support incremental compilation yet. E.g, Common / Metadata.
209     private val isIncremental = options.incremental && lookupTracker is DualLookupTracker
210     private val PATH_CONVERTER = RelativeFileToPathConverter(baseDir)
211 
212     private val symbolLookupTracker = (lookupTracker as? DualLookupTracker)?.symbolTracker ?: LookupTracker.DO_NOTHING
213     private val symbolLookupCacheDir = File(options.cachesDir, "symbolLookups")
214     private val symbolLookupCache = LookupStorage(symbolLookupCacheDir, PATH_CONVERTER)
215 
216     // TODO: rewrite LookupStorage to share file-to-id, etc.
217     private val classLookupTracker = (lookupTracker as? DualLookupTracker)?.classTracker ?: LookupTracker.DO_NOTHING
218     private val classLookupCacheDir = File(options.cachesDir, "classLookups")
219     private val classLookupCache = LookupStorage(classLookupCacheDir, PATH_CONVERTER)
220 
221     private val sourceToOutputsMap = FileToFilesMap(File(options.cachesDir, "sourceToOutputs"))
222 
223     private fun String.toRelativeFile() = File(this).relativeTo(baseDir)
224     private val KSFile.relativeFile
225         get() = filePath.toRelativeFile()
226 
227     private fun collectDefinedSymbols(ksFiles: Collection<KSFile>) {
228         ksFiles.forEach { file ->
229             file.accept(symbolCollector) {
230                 updatedSymbols.putValue(file.relativeFile, it)
231             }
232         }
233     }
234 
235     private val removedOutputsKey = File("<This is a virtual key for removed outputs; DO NOT USE>")
236 
237     private fun updateFromRemovedOutputs() {
238         val removedOutputs = sourceToOutputsMap.get(removedOutputsKey) ?: return
239 
240         symbolLookupCache.removeLookupsFrom(removedOutputs.asSequence())
241         classLookupCache.removeLookupsFrom(removedOutputs.asSequence())
242         removedOutputs.forEach {
243             symbolsMap.remove(it)
244             sealedMap.remove(it)
245         }
246 
247         sourceToOutputsMap.remove(removedOutputsKey)
248     }
249 
250     private fun updateLookupCache(dirtyFiles: Collection<File>) {
251         symbolLookupCache.update(symbolLookupTracker, dirtyFiles, options.knownRemoved)
252         symbolLookupCache.flush(false)
253         symbolLookupCache.close()
254 
255         classLookupCache.update(classLookupTracker, dirtyFiles, options.knownRemoved)
256         classLookupCache.flush(false)
257         classLookupCache.close()
258     }
259 
260     private fun logSourceToOutputs(outputs: Set<File>, sourceToOutputs: Map<File, Set<File>>) {
261         if (!options.incrementalLog)
262             return
263 
264         val logFile = File(logsDir, "kspSourceToOutputs.log")
265         logFile.appendText("=== Build $buildTime ===\n")
266         logFile.appendText("Accumulated source to outputs map\n")
267         sourceToOutputsMap.keys.forEach { source ->
268             logFile.appendText("  $source:\n")
269             sourceToOutputsMap[source]!!.forEach { output ->
270                 logFile.appendText("    $output\n")
271             }
272         }
273         logFile.appendText("\n")
274 
275         logFile.appendText("Reprocessed sources and their outputs\n")
276         sourceToOutputs.forEach { (source, outputs) ->
277             logFile.appendText("  $source:\n")
278             outputs.forEach {
279                 logFile.appendText("    $it\n")
280             }
281         }
282         logFile.appendText("\n")
283 
284         // Can be larger than the union of the above, because some outputs may have no source.
285         logFile.appendText("All reprocessed outputs\n")
286         outputs.forEach {
287             logFile.appendText("  $it\n")
288         }
289         logFile.appendText("\n")
290     }
291 
292     private fun logDirtyFiles(
293         files: Collection<KSFile>,
294         allFiles: Collection<KSFile>,
295         removedOutputs: Collection<File> = emptyList(),
296         dirtyFilesByCP: Collection<File> = emptyList(),
297         dirtyFilesByNewSyms: Collection<File> = emptyList(),
298         dirtyFilesBySealed: Collection<File> = emptyList(),
299     ) {
300         if (!options.incrementalLog)
301             return
302 
303         val logFile = File(logsDir, "kspDirtySet.log")
304         logFile.appendText("=== Build $buildTime ===\n")
305         logFile.appendText("All Files\n")
306         allFiles.forEach { logFile.appendText("  ${it.relativeFile}\n") }
307         logFile.appendText("Modified\n")
308         modified.forEach { logFile.appendText("  $it\n") }
309         logFile.appendText("Removed\n")
310         removed.forEach { logFile.appendText("  $it\n") }
311         logFile.appendText("Disappeared Outputs\n")
312         removedOutputs.forEach { logFile.appendText("  $it\n") }
313         logFile.appendText("Affected By CP\n")
314         dirtyFilesByCP.forEach { logFile.appendText("  $it\n") }
315         logFile.appendText("Affected By new syms\n")
316         dirtyFilesByNewSyms.forEach { logFile.appendText("  $it\n") }
317         logFile.appendText("Affected By sealed\n")
318         dirtyFilesBySealed.forEach { logFile.appendText("  $it\n") }
319         logFile.appendText("CP changes\n")
320         options.changedClasses.forEach { logFile.appendText("  $it\n") }
321         logFile.appendText("Dirty:\n")
322         files.forEach {
323             logFile.appendText("  ${it.relativeFile}\n")
324         }
325         val percentage = "%.2f".format(files.size.toDouble() / allFiles.size.toDouble() * 100)
326         logFile.appendText("\nDirty / All: $percentage%\n\n")
327     }
328 
329     // Beware: no side-effects here; Caches should only be touched in updateCaches.
330     fun calcDirtyFiles(ksFiles: List<KSFile>): Collection<KSFile> = closeFilesOnException {
331         if (!isIncremental) {
332             return ksFiles
333         }
334 
335         if (rebuild) {
336             collectDefinedSymbols(ksFiles)
337             logDirtyFiles(ksFiles, ksFiles)
338             return ksFiles
339         }
340 
341         val newSyms = mutableSetOf<LookupSymbol>()
342 
343         // Parse and add newly defined symbols in modified files.
344         ksFiles.filter { it.relativeFile in modified }.forEach { file ->
345             file.accept(symbolCollector) {
346                 updatedSymbols.putValue(file.relativeFile, it)
347                 newSyms.add(it)
348             }
349         }
350 
351         val dirtyFilesByNewSyms = newSyms.flatMap {
352             symbolLookupCache.get(it).map { File(it) }
353         }
354 
355         val dirtyFilesBySealed = sealedMap.keys.flatMap { sealedMap[it]!! }.flatMap {
356             symbolLookupCache.get(it).map { File(it) }
357         }
358 
359         // Calculate dirty files by dirty classes in CP.
360         val dirtyFilesByCP = options.changedClasses.flatMap { fqn ->
361             val name = fqn.substringAfterLast('.')
362             val scope = fqn.substringBeforeLast('.', "<anonymous>")
363             classLookupCache.get(LookupSymbol(name, scope)).map { File(it) } +
364                 symbolLookupCache.get(LookupSymbol(name, scope)).map { File(it) }
365         }.toSet()
366 
367         // output files that exist in CURR~2 but not in CURR~1
368         val removedOutputs = sourceToOutputsMap.get(removedOutputsKey) ?: emptyList()
369 
370         val noSourceFiles = options.changedClasses.map { fqn ->
371             NoSourceFile(baseDir, fqn).filePath.toRelativeFile()
372         }.toSet()
373 
374         val initialSet = mutableSetOf<File>()
375         initialSet.addAll(modified)
376         initialSet.addAll(removed)
377         initialSet.addAll(removedOutputs)
378         initialSet.addAll(dirtyFilesByCP)
379         initialSet.addAll(dirtyFilesByNewSyms)
380         initialSet.addAll(dirtyFilesBySealed)
381         initialSet.addAll(noSourceFiles)
382 
383         // modified can be seen as removed + new. Therefore the following check doesn't work:
384         //   if (modified.any { it !in sourceToOutputsMap.keys }) ...
385         if (modified.isNotEmpty() || options.changedClasses.isNotEmpty()) {
386             initialSet.add(anyChangesWildcard)
387         }
388 
389         val dirtyFiles = DirtinessPropagator(
390             symbolLookupCache,
391             symbolsMap,
392             sourceToOutputsMap,
393             anyChangesWildcard,
394             removedOutputsKey
395         ).propagate(initialSet)
396 
397         updateFromRemovedOutputs()
398 
399         logDirtyFiles(
400             ksFiles.filter { it.relativeFile in dirtyFiles },
401             ksFiles,
402             removedOutputs,
403             dirtyFilesByCP,
404             dirtyFilesByNewSyms,
405             dirtyFilesBySealed
406         )
407         return ksFiles.filter { it.relativeFile in dirtyFiles }
408     }
409 
410     private fun updateSourceToOutputs(
411         dirtyFiles: Collection<File>,
412         outputs: Set<File>,
413         sourceToOutputs: Map<File, Set<File>>,
414         removedOutputs: List<File>,
415     ) {
416         // Prune deleted sources in source-to-outputs map.
417         removed.forEach {
418             sourceToOutputsMap.remove(it)
419         }
420 
421         dirtyFiles.filterNot { sourceToOutputs.containsKey(it) }.forEach {
422             sourceToOutputsMap.remove(it)
423         }
424 
425         removedOutputs.forEach {
426             sourceToOutputsMap.remove(it)
427         }
428         sourceToOutputsMap[removedOutputsKey] = removedOutputs
429 
430         // Update source-to-outputs map from those reprocessed.
431         sourceToOutputs.forEach { src, outs ->
432             sourceToOutputsMap[src] = outs
433         }
434 
435         logSourceToOutputs(outputs, sourceToOutputs)
436 
437         sourceToOutputsMap.flush(false)
438     }
439 
440     private fun updateOutputs(outputs: Set<File>, cleanOutputs: Collection<File>) {
441         val outRoot = options.kspOutputDir
442         val bakRoot = File(options.cachesDir, "backups")
443 
444         fun File.abs() = File(baseDir, path)
445         fun File.bak() = File(bakRoot, abs().toRelativeString(outRoot))
446 
447         // Copy recursively, including last-modified-time of file and its parent dirs.
448         //
449         // `java.nio.file.Files.copy(path1, path2, options...)` keeps last-modified-time (if supported) according to
450         // https://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html
451         fun copy(src: File, dst: File, overwrite: Boolean) {
452             if (!dst.parentFile.exists())
453                 copy(src.parentFile, dst.parentFile, false)
454             if (overwrite) {
455                 Files.copy(
456                     src.toPath(),
457                     dst.toPath(),
458                     StandardCopyOption.COPY_ATTRIBUTES,
459                     StandardCopyOption.REPLACE_EXISTING
460                 )
461             } else {
462                 Files.copy(src.toPath(), dst.toPath(), StandardCopyOption.COPY_ATTRIBUTES)
463             }
464         }
465 
466         // Backing up outputs is necessary for two reasons:
467         //
468         // 1. Currently, outputs are always cleaned up in gradle plugin before compiler is called.
469         //    Untouched outputs need to be restore.
470         //
471         //    TODO: need a change in upstream to not clean files in gradle plugin.
472         //    Not cleaning files in gradle plugin has potentially fewer copies when processing succeeds.
473         //
474         // 2. Even if outputs are left from last compilation / processing, processors can still
475         //    fail and the outputs will need to be restored.
476 
477         // Backup
478         outputs.forEach { generated ->
479             copy(generated.abs(), generated.bak(), true)
480         }
481 
482         // Restore non-dirty outputs
483         cleanOutputs.forEach { dst ->
484             if (dst !in outputs) {
485                 copy(dst.bak(), dst.abs(), false)
486             }
487         }
488     }
489 
490     private fun updateCaches(dirtyFiles: Collection<File>, outputs: Set<File>, sourceToOutputs: Map<File, Set<File>>) {
491         // dirtyFiles may contain new files, which are unknown to sourceToOutputsMap.
492         val oldOutputs = dirtyFiles.flatMap { sourceToOutputsMap[it] ?: emptyList() }.distinct()
493         val removedOutputs = oldOutputs.filterNot { it in outputs }
494         updateSourceToOutputs(dirtyFiles, outputs, sourceToOutputs, removedOutputs)
495         updateLookupCache(dirtyFiles)
496 
497         // Update symbolsMap
498         fun <K : Comparable<K>, V> update(m: PersistentMap<K, Collection<V>>, u: MultiMap<K, V>) {
499             // Update symbol caches from modified files.
500             u.keySet().forEach {
501                 m.set(it, u[it].toSet())
502             }
503         }
504 
505         fun <K : Comparable<K>, V> remove(m: PersistentMap<K, Collection<V>>, removedKeys: Collection<K>) {
506             // Remove symbol caches from removed files.
507             removedKeys.forEach {
508                 m.remove(it)
509             }
510         }
511 
512         if (!rebuild) {
513             update(sealedMap, updatedSealed)
514             remove(sealedMap, removed)
515 
516             update(symbolsMap, updatedSymbols)
517             remove(symbolsMap, removed)
518         } else {
519             symbolsMap.clean()
520             update(symbolsMap, updatedSymbols)
521 
522             sealedMap.clean()
523             update(sealedMap, updatedSealed)
524         }
525         symbolsMap.flush(false)
526         symbolsMap.close()
527         sealedMap.flush(false)
528         sealedMap.close()
529     }
530 
531     fun registerGeneratedFiles(newFiles: Collection<KSFile>) = closeFilesOnException {
532         if (!isIncremental)
533             return@closeFilesOnException
534 
535         collectDefinedSymbols(newFiles)
536     }
537 
538     private inline fun <T> closeFilesOnException(f: () -> T): T {
539         try {
540             return f()
541         } catch (e: Exception) {
542             symbolsMap.close()
543             sealedMap.close()
544             symbolLookupCache.close()
545             classLookupCache.close()
546             sourceToOutputsMap.close()
547             throw e
548         }
549     }
550 
551     // TODO: add a wildcard for outputs with no source and get rid of the outputs parameter.
552     fun updateCachesAndOutputs(
553         dirtyFiles: Collection<KSFile>,
554         outputs: Set<File>,
555         sourceToOutputs: Map<File, Set<File>>,
556     ) = closeFilesOnException {
557         if (!isIncremental)
558             return
559 
560         cachesUpToDateFile.delete()
561         assert(!cachesUpToDateFile.exists())
562 
563         val dirtyFilePaths = dirtyFiles.map { it.relativeFile }
564 
565         updateCaches(dirtyFilePaths, outputs, sourceToOutputs)
566 
567         val cleanOutputs = mutableSetOf<File>()
568         sourceToOutputsMap.keys.forEach { source ->
569             if (source !in dirtyFilePaths && source != anyChangesWildcard && source != removedOutputsKey)
570                 cleanOutputs.addAll(sourceToOutputsMap[source]!!)
571         }
572         sourceToOutputsMap.close()
573         updateOutputs(outputs, cleanOutputs)
574 
575         cachesUpToDateFile.createNewFile()
576         assert(cachesUpToDateFile.exists())
577     }
578 
579     // Insert Java file -> names lookup records.
580     fun recordLookup(psiFile: PsiJavaFile, fqn: String) {
581         val path = psiFile.virtualFile.path
582         val name = fqn.substringAfterLast('.')
583         val scope = fqn.substringBeforeLast('.', "<anonymous>")
584 
585         // Java types are classes. Therefore lookups only happen in packages.
586         fun record(scope: String, name: String) =
587             symbolLookupTracker.record(path, Position.NO_POSITION, scope, ScopeKind.PACKAGE, name)
588 
589         record(scope, name)
590 
591         // If a resolved name is from some * import, it is overridable by some out-of-file changes.
592         // Therefore, the potential providers all need to be inserted. They are
593         //   1. definition of the name in the same package
594         //   2. other * imports
595         val onDemandImports =
596             psiFile.getOnDemandImports(false, false).mapNotNull { (it as? PsiPackage)?.qualifiedName }
597         if (scope in onDemandImports) {
598             record(psiFile.packageName, name)
599             onDemandImports.forEach {
600                 record(it, name)
601             }
602         }
603     }
604 
605     // Record a *leaf* type reference. This doesn't address type arguments.
606     private fun recordLookup(ref: PsiClassReferenceType, def: PsiClass) {
607         val psiFile = ref.reference.containingFile as? PsiJavaFile ?: return
608         // A type parameter doesn't have qualified name.
609         //
610         // Note that bounds of type parameters, or other references in classes,
611         // are not addressed recursively here. They are recorded in other places
612         // with more contexts, when necessary.
613         def.qualifiedName?.let { recordLookup(psiFile, it) }
614     }
615 
616     // Record a type reference, including its type arguments.
617     fun recordLookup(ref: PsiType) {
618         when (ref) {
619             is PsiArrayType -> recordLookup(ref.componentType)
620             is PsiClassReferenceType -> {
621                 val def = ref.resolve() ?: return
622                 recordLookup(ref, def)
623                 // in case the corresponding KotlinType is passed through ways other than KSTypeReferenceJavaImpl
624                 ref.typeArguments().forEach {
625                     if (it is PsiType) {
626                         recordLookup(it)
627                     }
628                 }
629             }
630             is PsiWildcardType -> ref.bound?.let { recordLookup(it) }
631         }
632     }
633 
634     // Record all references to super types (if they are written in Java) of a given type,
635     // in its type hierarchy.
636     fun recordLookupWithSupertypes(kotlinType: KotlinType) {
637         (listOf(kotlinType) + kotlinType.supertypes()).mapNotNull {
638             it.constructor.declarationDescriptor?.findPsi() as? PsiClass
639         }.forEach {
640             it.superTypes.forEach {
641                 recordLookup(it)
642             }
643         }
644     }
645 
646     // Record all type references in a Java field.
647     private fun recordLookupForJavaField(psi: PsiField) {
648         recordLookup(psi.type)
649     }
650 
651     // Record all type references in a Java method.
652     private fun recordLookupForJavaMethod(psi: PsiMethod) {
653         psi.parameterList.parameters.forEach {
654             recordLookup(it.type)
655         }
656         psi.returnType?.let { recordLookup(it) }
657         psi.typeParameters.forEach {
658             it.bounds.mapNotNull { it as? PsiType }.forEach {
659                 recordLookup(it)
660             }
661         }
662     }
663 
664     // Record all type references in a KSDeclaration
665     fun recordLookupForDeclaration(declaration: KSDeclaration) {
666         when (declaration) {
667             is KSPropertyDeclarationJavaImpl -> recordLookupForJavaField(declaration.psi)
668             is KSFunctionDeclarationJavaImpl -> recordLookupForJavaMethod(declaration.psi)
669         }
670     }
671 
672     // Record all type references in a CallableMemberDescriptor
673     fun recordLookupForCallableMemberDescriptor(descriptor: CallableMemberDescriptor) {
674         val psi = descriptor.findPsi()
675         when (psi) {
676             is PsiMethod -> recordLookupForJavaMethod(psi)
677             is PsiField -> recordLookupForJavaField(psi)
678         }
679     }
680 
681     // Record references from all declared functions in the type hierarchy of the given class.
682     // TODO: optimization: filter out inaccessible members
683     fun recordLookupForGetAllFunctions(descriptor: ClassDescriptor) {
684         recordLookupForGetAll(descriptor) {
685             it.methods.forEach {
686                 recordLookupForJavaMethod(it)
687             }
688         }
689     }
690 
691     // Record references from all declared fields in the type hierarchy of the given class.
692     // TODO: optimization: filter out inaccessible members
693     fun recordLookupForGetAllProperties(descriptor: ClassDescriptor) {
694         recordLookupForGetAll(descriptor) {
695             it.fields.forEach {
696                 recordLookupForJavaField(it)
697             }
698         }
699     }
700 
701     fun recordLookupForGetAll(descriptor: ClassDescriptor, doChild: (PsiClass) -> Unit) {
702         (descriptor.getAllSuperclassesWithoutAny() + descriptor).mapNotNull {
703             it.findPsi() as? PsiClass
704         }.forEach { psiClass ->
705             psiClass.superTypes.forEach {
706                 recordLookup(it)
707             }
708             doChild(psiClass)
709         }
710     }
711 
712     fun recordGetSealedSubclasses(classDeclaration: KSClassDeclaration) {
713         val name = classDeclaration.simpleName.asString()
714         val scope = classDeclaration.qualifiedName?.asString()
715             ?.let { it.substring(0, Math.max(it.length - name.length - 1, 0)) } ?: return
716         updatedSealed.putValue(classDeclaration.containingFile!!.relativeFile, LookupSymbol(name, scope))
717     }
718 
719     // Debugging and testing only.
720     fun dumpLookupRecords(): Map<String, List<String>> {
721         val map = mutableMapOf<String, List<String>>()
722         (symbolLookupTracker as LookupTrackerImpl).lookups.entrySet().forEach { e ->
723             val key = "${e.key.scope}.${e.key.name}"
724             map[key] = e.value.map { PATH_CONVERTER.toFile(it).path }
725         }
726         return map
727     }
728 }
729 
730 internal class DirtinessPropagator(
731     private val lookupCache: LookupStorage,
732     private val symbolsMap: FileToSymbolsMap,
733     private val sourceToOutputs: FileToFilesMap,
734     private val anyChangesWildcard: File,
735     private val removedOutputsKey: File
736 ) {
737     private val visitedFiles = mutableSetOf<File>()
738     private val visitedSyms = mutableSetOf<LookupSymbol>()
739 
<lambda>null740     private val outputToSources = mutableMapOf<File, MutableSet<File>>().apply {
741         sourceToOutputs.keys.forEach { source ->
742             if (source != anyChangesWildcard && source != removedOutputsKey) {
743                 sourceToOutputs[source]!!.forEach { output ->
744                     getOrPut(output) { mutableSetOf() }.add(source)
745                 }
746             }
747         }
748     }
749 
visitnull750     private fun visit(sym: LookupSymbol) {
751         if (sym in visitedSyms)
752             return
753         visitedSyms.add(sym)
754 
755         lookupCache.get(sym).forEach {
756             visit(File(it))
757         }
758     }
759 
visitnull760     private fun visit(file: File) {
761         if (file in visitedFiles)
762             return
763         visitedFiles.add(file)
764 
765         // Propagate by dependencies
766         symbolsMap[file]?.forEach {
767             visit(it)
768         }
769 
770         // Propagate by input-output relations
771         // Given (..., I, ...) -> O:
772         // 1) if I is dirty, then O is dirty.
773         // 2) if O is dirty, then O must be regenerated, which requires all of its inputs to be reprocessed.
774         sourceToOutputs[file]?.forEach {
775             visit(it)
776         }
777         outputToSources[file]?.forEach {
778             visit(it)
779         }
780     }
781 
propagatenull782     fun propagate(initialSet: Collection<File>): Set<File> {
783         initialSet.forEach { visit(it) }
784         return visitedFiles
785     }
786 }
787