Implement persistent storage for script class path roots (KT-35886)

Save absolute paths instead of getCanonicalPath usages

^KT-34444
^KT-35886 Fixed
This commit is contained in:
Natalia Selezneva
2020-01-15 11:08:27 +03:00
parent c59e794ab4
commit a373ab96d4
3 changed files with 172 additions and 52 deletions
@@ -17,13 +17,20 @@ import com.intellij.psi.search.NonClasspathDirectoriesScope
import com.intellij.util.containers.ConcurrentFactoryMap
import org.jetbrains.kotlin.idea.caches.project.getAllProjectSdks
import org.jetbrains.kotlin.idea.core.script.ScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.debug
import org.jetbrains.kotlin.scripting.resolve.ScriptCompilationConfigurationWrapper
import java.io.File
internal class ScriptClassRootsCache(
private val project: Project,
private val all: Map<VirtualFile, ScriptCompilationConfigurationWrapper>
) {
private val scriptsSdksCache: Map<VirtualFile, Sdk?> =
ConcurrentFactoryMap.createWeakMap { file ->
return@createWeakMap getScriptSdk(all[file]) ?: ScriptConfigurationManager.getScriptDefaultSdk(project)
}
fun getScriptSdk(file: VirtualFile): Sdk? = scriptsSdksCache[file]
private fun getScriptSdk(compilationConfiguration: ScriptCompilationConfigurationWrapper?): Sdk? {
// workaround for mismatched gradle wrapper and plugin version
val javaHome = try {
@@ -35,48 +42,21 @@ internal class ScriptClassRootsCache(
return getAllProjectSdks().find { it.homeDirectory == javaHome }
}
private val scriptsSdksCache: Map<VirtualFile, Sdk?> =
ConcurrentFactoryMap.createWeakMap { file ->
val compilationConfiguration = all[file]
return@createWeakMap getScriptSdk(compilationConfiguration)
?: ScriptConfigurationManager.getScriptDefaultSdk(project)
}
fun getScriptSdk(file: VirtualFile): Sdk? = scriptsSdksCache[file]
val firstScriptSdk: Sdk? by lazy {
val firstCachedScript = all.keys.firstOrNull() ?: return@lazy null
return@lazy getScriptSdk(firstCachedScript)
}
private val allSdks by lazy {
all.mapNotNull { scriptsSdksCache[it.key] }
.distinct()
}
private val allNonIndexedSdks by lazy {
all.mapNotNull { scriptsSdksCache[it.key] }
.filterNonModuleSdk()
.distinct()
}
private fun List<Sdk>.filterNonModuleSdk(): List<Sdk> {
val moduleSdks = ModuleManager.getInstance(project).modules.map { ModuleRootManager.getInstance(it).sdk }
return filterNot { moduleSdks.contains(it) }
private fun Sdk.isAlreadyIndexed(): Boolean {
return ModuleManager.getInstance(project).modules.any { ModuleRootManager.getInstance(it).sdk == this }
}
val allDependenciesClassFiles by lazy {
val sdkFiles = allNonIndexedSdks.flatMap { it.rootProvider.getFiles(OrderRootType.CLASSES).toList() }
val scriptDependenciesClasspath = all.flatMap { it.value.dependenciesClassPath }.distinct()
sdkFiles + ScriptConfigurationManager.toVfsRoots(scriptDependenciesClasspath)
ScriptClassRootsStorage.getInstance(project).loadClasspathRoots()
}
val allDependenciesSources by lazy {
val sdkSources = allNonIndexedSdks.flatMap { it.rootProvider.getFiles(OrderRootType.SOURCES).toList() }
val scriptDependenciesSources = all.flatMap { it.value.dependenciesSources }.distinct()
sdkSources + ScriptConfigurationManager.toVfsRoots(scriptDependenciesSources)
ScriptClassRootsStorage.getInstance(project).loadSourcesRoots()
}
val allDependenciesClassFilesScope by lazy {
@@ -116,32 +96,55 @@ internal class ScriptClassRootsCache(
}
fun hasNotCachedRoots(compilationConfiguration: ScriptCompilationConfigurationWrapper): Boolean {
val scriptSdk = getScriptSdk(compilationConfiguration)
?: ScriptConfigurationManager.getScriptDefaultSdk(project)
return !ScriptClassRootsStorage.getInstance(project).containsAll(extractRoots(compilationConfiguration))
}
val wasSdkChanged = scriptSdk != null && !allSdks.contains(scriptSdk)
if (wasSdkChanged) {
debug { "sdk was changed: $compilationConfiguration" }
return true
private fun extractRoots(configuration: ScriptCompilationConfigurationWrapper): ScriptClassRootsStorage.Companion.ScriptClassRoots {
val scriptSdk = getScriptSdk(configuration) ?: ScriptConfigurationManager.getScriptDefaultSdk(project)
if (scriptSdk != null && !scriptSdk.isAlreadyIndexed()) {
return ScriptClassRootsStorage.Companion.ScriptClassRoots(
configuration.dependenciesClassPath,
configuration.dependenciesSources,
listOf(scriptSdk)
)
}
val newClassRoots = ScriptConfigurationManager.toVfsRoots(compilationConfiguration.dependenciesClassPath)
for (newClassRoot in newClassRoots) {
if (!allDependenciesClassFiles.contains(newClassRoot)) {
debug { "class root was changed: $newClassRoot" }
return true
return ScriptClassRootsStorage.Companion.ScriptClassRoots(
configuration.dependenciesClassPath,
configuration.dependenciesSources,
emptyList()
)
}
init {
saveClassRootsToStorage()
}
private fun saveClassRootsToStorage() {
if (all.isEmpty()) return
val classpath = mutableSetOf<File>()
val sources = mutableSetOf<File>()
val sdks = mutableSetOf<Sdk>()
for ((file, configuration) in all) {
val scriptSdk = getScriptSdk(file)
if (scriptSdk != null && !scriptSdk.isAlreadyIndexed()) {
sdks.add(scriptSdk)
}
classpath.addAll(configuration.dependenciesClassPath)
sources.addAll(configuration.dependenciesSources)
}
val newSourceRoots = ScriptConfigurationManager.toVfsRoots(compilationConfiguration.dependenciesSources)
for (newSourceRoot in newSourceRoots) {
if (!allDependenciesSources.contains(newSourceRoot)) {
debug { "source root was changed: $newSourceRoot" }
return true
}
}
return false
val rootsStorage = ScriptClassRootsStorage.getInstance(project)
rootsStorage.save(
ScriptClassRootsStorage.Companion.ScriptClassRoots(
classpath.toList(),
sources.toList(),
sdks.toList()
)
)
}
fun contains(file: VirtualFile) = all.containsKey(file)
@@ -0,0 +1,115 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.idea.core.script.configuration.utils
import com.intellij.openapi.components.*
import com.intellij.openapi.project.Project
import com.intellij.openapi.projectRoots.ProjectJdkTable
import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.OrderRootType
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.io.URLUtil
import com.intellij.util.xmlb.XmlSerializerUtil
import org.jetbrains.kotlin.idea.core.script.debug
import java.io.File
@State(
name = "ScriptClassRootsStorage",
storages = [Storage(StoragePathMacros.CACHE_FILE)]
)
class ScriptClassRootsStorage : PersistentStateComponent<ScriptClassRootsStorage> {
private var classpath: Set<String> = hashSetOf()
private var sources: Set<String> = hashSetOf()
private var sdks: Set<String> = hashSetOf()
override fun getState(): ScriptClassRootsStorage? {
return this
}
override fun loadState(state: ScriptClassRootsStorage) {
XmlSerializerUtil.copyBean(state, this)
}
private fun toStringNames(sdks: Collection<Sdk>): Set<String> {
return sdks.map { it.name }.toSet()
}
private fun toStringValues(prop: Collection<File>): Set<String> {
return prop.mapNotNull {
when {
it.isDirectory -> it.absolutePath
it.isFile -> it.absolutePath + URLUtil.JAR_SEPARATOR
else -> null
}
}.toSet()
}
private fun toVirtualFiles(prop: Set<String>, sources: Boolean): List<VirtualFile> {
val rootType = if (sources) OrderRootType.SOURCES else OrderRootType.CLASSES
return prop.mapNotNull { ProjectJdkTable.getInstance().findJdk(it) }
.flatMap { it.rootProvider.getFiles(rootType).toList() }
}
private fun toVirtualFiles(prop: Collection<String>): List<VirtualFile> {
return prop.mapNotNull {
StandardFileSystems.local()?.findFileByPath(it)?.let {
return@mapNotNull it
}
StandardFileSystems.jar()?.findFileByPath(it)?.let {
return@mapNotNull it
}
// TODO: report this somewhere, but do not throw: assert(res != null, { "Invalid classpath entry '$this': exists: ${exists()}, is directory: $isDirectory, is file: $isFile" })
null
}.distinct()
}
fun containsAll(configuration: ScriptClassRoots): Boolean {
if (!classpath.containsAll(toStringValues(configuration.classpathFiles))) {
debug { "class roots were changed: old = $classpath, new = ${configuration.classpathFiles}" }
return false
}
if (!sources.containsAll(toStringValues(configuration.sourcesFiles))) {
debug { "source roots were changed: old = $sources, new = ${configuration.sourcesFiles}" }
return false
}
if (!sdks.containsAll(toStringNames(configuration.sdks))) {
debug { "sdk classes were changed: old = $sdks, new = ${configuration.sdks.map { it.homePath }}" }
return false
}
return true
}
fun save(configuration: ScriptClassRoots) {
// TODO: do not drop all storage on save: KT-34444
classpath = toStringValues(configuration.classpathFiles)
sources = toStringValues(configuration.sourcesFiles)
sdks = toStringNames(configuration.sdks)
}
fun loadClasspathRoots(): List<VirtualFile> {
return toVirtualFiles(sdks, false) + toVirtualFiles(classpath)
}
fun loadSourcesRoots(): List<VirtualFile> {
return toVirtualFiles(sdks, true) + toVirtualFiles(sources)
}
companion object {
fun getInstance(project: Project): ScriptClassRootsStorage =
ServiceManager.getService(project, ScriptClassRootsStorage::class.java)
data class ScriptClassRoots(
val classpathFiles: List<File>,
val sourcesFiles: List<File>,
val sdks: List<Sdk>
)
}
}
@@ -1,5 +1,7 @@
<idea-plugin>
<extensions defaultExtensionNs="com.intellij">
<projectService serviceImplementation="org.jetbrains.kotlin.idea.core.script.configuration.utils.ScriptClassRootsStorage"/>
<trafficLightRendererContributor implementation="org.jetbrains.kotlin.idea.core.script.ScriptTrafficLightRendererContributor"/>
</extensions>
</idea-plugin>