Refactor script dependencies management

This commit is contained in:
Ilya Chernikov
2016-06-09 19:56:37 +02:00
committed by Pavel V. Talanov
parent c244414f3c
commit 082290f8e3
23 changed files with 470 additions and 510 deletions
@@ -23,27 +23,22 @@ import com.intellij.openapi.projectRoots.Sdk
import com.intellij.openapi.roots.*
import com.intellij.openapi.roots.impl.libraries.LibraryEx
import com.intellij.openapi.roots.libraries.Library
import com.intellij.openapi.roots.libraries.LibraryTable
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VfsUtilCore
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.psi.search.DelegatingGlobalSearchScope
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.util.SmartList
import com.intellij.util.io.URLUtil
import org.jdom.Element
import org.jetbrains.kotlin.analyzer.ModuleInfo
import org.jetbrains.kotlin.descriptors.ModuleDescriptor
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.script.KotlinScriptDefinition
import org.jetbrains.kotlin.script.KotlinScriptExtraImport
import org.jetbrains.kotlin.utils.alwaysNull
import org.jetbrains.kotlin.utils.emptyOrSingletonList
import java.io.File
import java.io.FileNotFoundException
import java.lang.reflect.Method
import java.util.*
@@ -297,8 +292,7 @@ class ScriptModuleSearchScope(val scriptFile: VirtualFile, baseScope: GlobalSear
}
internal data class ScriptModuleInfo(val project: Project, val module: Module?, val scriptFile: VirtualFile,
val scriptDefinition: KotlinScriptDefinition,
val scriptExtraImports: List<KotlinScriptExtraImport>) : IdeaModuleInfo {
val scriptDefinition: KotlinScriptDefinition) : IdeaModuleInfo {
override val moduleOrigin: ModuleOrigin
get() = ModuleOrigin.OTHER
@@ -315,16 +309,17 @@ internal data class ScriptModuleInfo(val project: Project, val module: Module?,
private fun dependenciesRoots(): List<VirtualFile> {
// TODO: find out whether it should be cashed (some changes listener should be implemented for the cached roots)
val jarfs = StandardFileSystems.jar()
return (scriptDefinition.getScriptDependenciesClasspath() + scriptExtraImports.flatMap { it.classpath })
.map { File(it).canonicalFile }
.distinct()
.mapNotNull {
// TODO: ensure that the entries are checked elsewhere, so diagnostics is delivered to a user if files are not correctly specified
if (it.isFile)
jarfs.findFileByPath(it.absolutePath + URLUtil.JAR_SEPARATOR) ?: null // diag: Classpath entry points to a file that is not a JAR archive
else
StandardFileSystems.local().findFileByPath(it.absolutePath) ?: null // diag: Classpath entry points to a non-existent location
}
return scriptDefinition.getDependenciesFor(scriptFile, project)?.classpath
?.map { File(it).canonicalFile }
?.distinct()
?.mapNotNull {
// TODO: ensure that the entries are checked elsewhere, so diagnostics is delivered to a user if files are not correctly specified
if (it.isFile)
jarfs.findFileByPath(it.absolutePath + URLUtil.JAR_SEPARATOR) ?: null // diag: Classpath entry points to a file that is not a JAR archive
else
StandardFileSystems.local().findFileByPath(it.absolutePath) ?: null // diag: Classpath entry points to a non-existent location
}
?: emptyList()
}
override fun dependencies() =
@@ -30,7 +30,6 @@ import org.jetbrains.kotlin.idea.util.ProjectRootsUtil
import org.jetbrains.kotlin.psi.*
import org.jetbrains.kotlin.psi.psiUtil.getParentOfType
import org.jetbrains.kotlin.script.getScriptDefinition
import org.jetbrains.kotlin.script.getScriptExtraImports
import org.jetbrains.kotlin.utils.sure
fun PsiElement.getModuleInfo(): IdeaModuleInfo = this.getModuleInfo { reason ->
@@ -125,7 +124,7 @@ private fun getModuleInfoByVirtualFile(project: Project, virtualFile: VirtualFil
val scriptDefinition = getScriptDefinition(virtualFile, project)
if (scriptDefinition != null)
return ScriptModuleInfo(project, module, virtualFile, scriptDefinition, getScriptExtraImports(virtualFile, project))
return ScriptModuleInfo(project, module, virtualFile, scriptDefinition)
return NotUnderContentRootModuleInfo
}
@@ -22,7 +22,7 @@ import org.jetbrains.kotlin.builtins.DefaultBuiltIns
import org.jetbrains.kotlin.descriptors.*
import org.jetbrains.kotlin.idea.decompiler.KtDecompiledFile
import org.jetbrains.kotlin.idea.decompiler.textBuilder.DecompiledTextIndexer
import org.jetbrains.kotlin.idea.script.KotlinScriptConfigurationManager
import org.jetbrains.kotlin.idea.core.script.KotlinScriptConfigurationManager
import org.jetbrains.kotlin.idea.stubindex.KotlinFullClassNameIndex
import org.jetbrains.kotlin.idea.stubindex.KotlinSourceFilterScope
import org.jetbrains.kotlin.idea.stubindex.KotlinTopLevelFunctionFqnNameIndex
@@ -1,164 +0,0 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.script
import com.intellij.openapi.components.AbstractProjectComponent
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ex.ProjectRootManagerEx
import com.intellij.openapi.util.EmptyRunnable
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.VirtualFileManager
import com.intellij.openapi.vfs.newvfs.BulkFileListener
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.indexing.IndexableSetContributor
import com.intellij.util.io.URLUtil
import org.jetbrains.kotlin.idea.caches.resolve.FileLibraryScope
import org.jetbrains.kotlin.script.*
import org.jetbrains.kotlin.utils.PathUtil
import java.io.File
import java.io.FileNotFoundException
import java.lang.ref.WeakReference
import java.util.*
import kotlin.concurrent.read
import kotlin.concurrent.write
@Suppress("unused") // project component
class KotlinScriptConfigurationManager(project: Project,
private val scriptDefinitionProvider: KotlinScriptDefinitionProvider,
private val scriptExtraImportsProvider: KotlinScriptExtraImportsProvider?,
private val kotlinScriptDependenciesIndexableSetContributor: KotlinScriptDependenciesIndexableSetContributor?
) : AbstractProjectComponent(project) {
private val kotlinEnvVars: Map<String, List<String>> by lazy { generateKotlinScriptClasspathEnvVarsFromPaths(myProject, PathUtil.getKotlinPathsForIdeaPlugin()) }
init {
reloadScriptDefinitions()
// TODO: get rid of this expensive call as soon as makeRootsChange call will work reliably
cacheAllScriptsExtraImports()
val weakThis = WeakReference(this)
myProject.messageBus.connect().subscribe(VirtualFileManager.VFS_CHANGES, object : BulkFileListener.Adapter() {
override fun after(events: List<VFileEvent>) {
val changedExtraImportConfigs = ArrayList<VirtualFile>()
var anyScriptDefinitionChanged = false
events.forEach {
it.file?.let {
if (isScriptDefinitionConfigFile(it)) {
anyScriptDefinitionChanged = true
}
else {
weakThis.get()?.scriptExtraImportsProvider?.run {
if (isExtraImportsConfig(it)) {
changedExtraImportConfigs.add(it)
}
}
}
}
}
if (anyScriptDefinitionChanged) {
weakThis.get()?.reloadScriptDefinitions()
}
if (changedExtraImportConfigs.isNotEmpty()) {
weakThis.get()?.scriptExtraImportsProvider?.invalidateExtraImportsByImportsFiles(changedExtraImportConfigs)
}
}
})
// omitting case then scriptExtraImportsProvider is not configured, considering it happens only in tests
scriptExtraImportsProvider?.subscribeOnExtraImportsChanged { files ->
weakThis.get()?.apply {
cacheLock.write {
allScriptsClasspathCache = null
}
ProjectRootManagerEx.getInstanceEx(myProject)?.makeRootsChange(EmptyRunnable.getInstance(), false, true)
}
}
}
private var allScriptsClasspathCache: List<VirtualFile>? = null
private val cacheLock = java.util.concurrent.locks.ReentrantReadWriteLock()
fun getScriptClasspath(file: VirtualFile): List<VirtualFile> =
scriptExtraImportsProvider
?.getExtraImports(file)
?.flatMap { it.classpath }
?.map { it.classpathEntryToVfs() }
?: emptyList()
fun getAllScriptsClasspath(): List<VirtualFile> = cacheLock.read {
if (allScriptsClasspathCache == null) {
allScriptsClasspathCache =
(scriptExtraImportsProvider?.getKnownCombinedClasspath() ?: emptyList())
.distinct()
.mapNotNull { it.classpathEntryToVfs() }
}
return allScriptsClasspathCache!!
}
private fun String.classpathEntryToVfs(): VirtualFile =
if (File(this).isDirectory)
StandardFileSystems.local()?.findFileByPath(this) ?: throw FileNotFoundException("Classpath entry points to a non-existent location: ${this}")
else
StandardFileSystems.jar()?.findFileByPath(this + URLUtil.JAR_SEPARATOR) ?: throw FileNotFoundException("Classpath entry points to a file that is not a JAR archive: ${this}")
fun getAllScriptsClasspathScope(): GlobalSearchScope? {
return getAllScriptsClasspath().let { cp ->
if (cp.isEmpty()) null
else GlobalSearchScope.union(cp.map { FileLibraryScope(myProject, it) }.toTypedArray())
}
}
private fun reloadScriptDefinitions() {
loadScriptConfigsFromProjectRoot(File(myProject.basePath ?: ".")).let {
if (it.isNotEmpty()) {
scriptDefinitionProvider.setScriptDefinitions(
it.map { KotlinConfigurableScriptDefinition(it, kotlinEnvVars) } + StandardScriptDefinition)
}
}
}
private fun cacheAllScriptsExtraImports() {
fun<R> VirtualFile.vfsWalkFiles(onFile: (VirtualFile) -> List<R>?): List<R> {
assert(isDirectory)
return children.flatMap { when {
it.isDirectory -> it.vfsWalkFiles(onFile)
else -> onFile(it) ?: emptyList()
} }
}
myProject.baseDir.vfsWalkFiles {
scriptExtraImportsProvider?.getExtraImports(it)
}
}
companion object {
@JvmStatic
fun getInstance(project: Project): KotlinScriptConfigurationManager =
project.getComponent(KotlinScriptConfigurationManager::class.java)
}
}
class KotlinScriptDependenciesIndexableSetContributor : IndexableSetContributor() {
override fun getAdditionalProjectRootsToIndex(project: Project): Set<VirtualFile> =
KotlinScriptConfigurationManager.getInstance(project).getAllScriptsClasspath().toSet()
override fun getAdditionalRootsToIndex(): Set<VirtualFile> = emptySet()
}
@@ -1,76 +0,0 @@
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.idea.script
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.roots.impl.PackageDirectoryCache
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.NonClasspathClassFinder
import com.intellij.psi.PsiClass
import com.intellij.psi.search.EverythingGlobalScope
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.util.containers.ConcurrentFactoryMap
import org.jetbrains.kotlin.idea.caches.resolve.ScriptModuleSearchScope
import org.jetbrains.kotlin.load.java.JavaClassFinderImpl
import org.jetbrains.kotlin.resolve.jvm.KotlinSafeClassFinder
class KotlinScriptDependenciesClassFinder(project: Project,
private val kotlinScriptConfigurationManager: KotlinScriptConfigurationManager
) : NonClasspathClassFinder(project), KotlinSafeClassFinder {
private val myCaches = object : ConcurrentFactoryMap<VirtualFile, PackageDirectoryCache>() {
override fun create(file: VirtualFile): PackageDirectoryCache? {
val scriptClasspath = kotlinScriptConfigurationManager.getScriptClasspath(file)
val v = NonClasspathClassFinder.createCache(scriptClasspath)
return v
}
}
override fun calcClassRoots(): List<VirtualFile> = kotlinScriptConfigurationManager.getAllScriptsClasspath()
override fun getCache(scope: GlobalSearchScope?): PackageDirectoryCache =
(scope as? ScriptModuleSearchScope ?:
(scope as? JavaClassFinderImpl.FilterOutKotlinSourceFilesScope)?.base as? ScriptModuleSearchScope
)?.let {
myCaches.get(it.scriptFile)
} ?: super.getCache(scope)
override fun clearCache() {
super.clearCache()
myCaches.clear()
}
override fun findClass(qualifiedName: String, scope: GlobalSearchScope): PsiClass? =
super.findClass(qualifiedName, scope)?.let { aClass ->
when {
scope is ScriptModuleSearchScope ||
(scope as? JavaClassFinderImpl.FilterOutKotlinSourceFilesScope)?.base is ScriptModuleSearchScope ||
scope is EverythingGlobalScope ||
aClass.containingFile?.virtualFile.let { file ->
file != null &&
with (ProjectFileIndex.SERVICE.getInstance(myProject)) {
!isInContent(file) &&
!isInLibraryClasses(file) &&
!isInLibrarySource(file)
}
} -> aClass
else -> null
}
}
}