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