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
@@ -34,7 +34,7 @@ import org.jetbrains.kotlin.resolve.scopes.ImportingScope
import org.jetbrains.kotlin.resolve.scopes.LexicalScope
import org.jetbrains.kotlin.resolve.scopes.SubpackagesImportingScope
import org.jetbrains.kotlin.resolve.source.KotlinSourceElement
import org.jetbrains.kotlin.script.getScriptExtraImports
import org.jetbrains.kotlin.script.getScriptExternalDependencies
import org.jetbrains.kotlin.storage.StorageManager
import org.jetbrains.kotlin.storage.getValue
import org.jetbrains.kotlin.types.TypeSubstitutor
@@ -73,7 +73,7 @@ class FileScopeFactory(
val explicitImportResolver = createImportResolver(ExplicitImportsIndexed(imports), bindingTrace)
val allUnderImportResolver = createImportResolver(AllUnderImportsIndexed(imports), bindingTrace)
val extraImports = ktImportsFactory.createImportDirectives(getScriptExtraImports(file).flatMap { it.names.map { ImportPath(it) } })
val extraImports = ktImportsFactory.createImportDirectives(getScriptExternalDependencies(file).flatMap { it.imports.map { ImportPath(it) } })
val allImplicitImports = defaultImports + extraImports
val defaultImportsFiltered = if (aliasImportNames.isEmpty()) { // optimization
@@ -17,14 +17,6 @@
package org.jetbrains.kotlin.script
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.util.xmlb.XmlSerializer
import com.intellij.util.xmlb.annotations.AbstractCollection
import com.intellij.util.xmlb.annotations.Tag
import org.jdom.Document
import org.jdom.Element
import org.jdom.output.Format
import org.jdom.output.XMLOutputter
import org.jetbrains.kotlin.descriptors.ScriptDescriptor
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.parsing.KotlinParserDefinition
@@ -54,7 +46,24 @@ data class KotlinConfigurableScriptDefinition(val config: KotlinScriptConfig, va
private val evaluatedClasspath by lazy { config.classpath.evalWithVars(environmentVars).distinct() }
override fun getScriptDependenciesClasspath(): List<String> = evaluatedClasspath
override fun <TF> getDependenciesFor(file: TF, project: Project): KotlinScriptExternalDependencies? =
if (!isScript(file)) null
else {
val extDeps = getScriptDependenciesFromConfig(file)
when {
extDeps != null ->
object : KotlinScriptExternalDependencies {
override val classpath = evaluatedClasspath + extDeps.classpath.evalWithVars(environmentVars).distinct()
override val imports = extDeps.imports
override val sources = extDeps.sources.evalWithVars(environmentVars).distinct()
}
!evaluatedClasspath.isEmpty() ->
object : KotlinScriptExternalDependencies {
override val classpath = evaluatedClasspath
}
else -> null
}
}
}
@@ -62,7 +71,7 @@ data class KotlinConfigurableScriptDefinition(val config: KotlinScriptConfig, va
// if corresponding list of replacements is empty, all strings containing the reference to the var are removed
// TODO: fix and tests
// TODO: move to some utils
internal fun List<String>.evalWithVars(varsMap: Map<String, List<String>>?): List<String> =
internal fun Iterable<String>.evalWithVars(varsMap: Map<String, List<String>>?): Iterable<String> =
if (varsMap == null || varsMap.isEmpty()) this
else this.flatMap { cpentry ->
varsMap.entries.fold(listOf(cpentry)) { p, v ->
@@ -16,10 +16,12 @@
package org.jetbrains.kotlin.script
import com.intellij.openapi.fileTypes.LanguageFileType
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.util.PathUtil
import org.jetbrains.kotlin.descriptors.ScriptDescriptor
import org.jetbrains.kotlin.idea.KotlinFileType
import org.jetbrains.kotlin.name.ClassId
import org.jetbrains.kotlin.name.FqName
import org.jetbrains.kotlin.name.Name
@@ -36,19 +38,39 @@ import java.io.File
import kotlin.reflect.KClass
interface KotlinScriptDefinition {
val name: String
val name: String get() = "Kotlin Script"
// TODO: consider creating separate type (subtype? for kotlin scripts)
val fileType: LanguageFileType get() = KotlinFileType.INSTANCE
fun <TF> isScript(file: TF): Boolean =
getFileName(file).endsWith(KotlinParserDefinition.STD_SCRIPT_EXT)
// TODO: replace these 3 functions with template property
fun getScriptParameters(scriptDescriptor: ScriptDescriptor): List<ScriptParameter>
fun getScriptSupertypes(scriptDescriptor: ScriptDescriptor): List<KotlinType> = emptyList()
fun getScriptParametersToPassToSuperclass(scriptDescriptor: ScriptDescriptor): List<Name> = emptyList()
fun <TF> isScript(file: TF): Boolean
fun getScriptName(script: KtScript): Name
fun getScriptDependenciesClasspath(): List<String> = emptyList()
fun getScriptName(script: KtScript): Name =
ScriptNameUtil.fileNameWithExtensionStripped(script, KotlinParserDefinition.STD_SCRIPT_EXT)
fun <TF> getDependenciesFor(file: TF, project: Project): KotlinScriptExternalDependencies? = null
}
interface KotlinScriptExternalDependencies {
val classpath: Iterable<String> get() = emptyList()
val imports: Iterable<String> get() = emptyList()
val sources: Iterable<String> get() = emptyList()
}
class KotlinScriptExternalDependenciesUnion(val dependencies: Iterable<KotlinScriptExternalDependencies>) : KotlinScriptExternalDependencies {
override val classpath: Iterable<String> get() = dependencies.flatMap { it.classpath }
override val imports: Iterable<String> get() = dependencies.flatMap { it.imports }
override val sources: Iterable<String> get() = dependencies.flatMap { it.sources }
}
data class ScriptParameter(val name: Name, val type: KotlinType)
fun <TF> getFileExtension(file: TF) = PathUtil.getFileExtension(getFileName(file))
fun <TF> getFileName(file: TF): String = when (file) {
is PsiFile -> file.originalFile.name
is VirtualFile -> file.name
@@ -56,17 +78,16 @@ fun <TF> getFileName(file: TF): String = when (file) {
else -> throw IllegalArgumentException("Unsupported file type $file")
}
fun <TF> getFilePath(file: TF): String = when (file) {
is PsiFile -> file.originalFile.run { virtualFile?.path ?: name } // TODO: replace name with path of PSI elements
is VirtualFile -> file.path
is File -> file.canonicalPath
else -> throw IllegalArgumentException("Unsupported file type $file")
}
object StandardScriptDefinition : KotlinScriptDefinition {
private val ARGS_NAME = Name.identifier("args")
override val name = "Kotlin Script"
override fun getScriptName(script: KtScript): Name =
ScriptNameUtil.fileNameWithExtensionStripped(script, KotlinParserDefinition.STD_SCRIPT_EXT)
override fun <TF> isScript(file: TF): Boolean =
getFileExtension(file) == KotlinParserDefinition.STD_SCRIPT_SUFFIX
// NOTE: for now we treat .kts files as if they have 'args: Array<String>' parameter
// this is not supposed to be final design
override fun getScriptParameters(scriptDescriptor: ScriptDescriptor): List<ScriptParameter> =
@@ -17,6 +17,7 @@
package org.jetbrains.kotlin.script
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.fileTypes.LanguageFileType
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
@@ -28,10 +29,8 @@ class KotlinScriptDefinitionProvider {
private val definitions: MutableList<KotlinScriptDefinition> = arrayListOf(StandardScriptDefinition)
private val definitionsLock = java.util.concurrent.locks.ReentrantReadWriteLock()
private val notificationHandlers = ArrayList<() -> Unit>()
private val handlersLock = java.util.concurrent.locks.ReentrantReadWriteLock()
fun setScriptDefinitions(newDefinitions: List<KotlinScriptDefinition>): Unit {
fun setScriptDefinitions(newDefinitions: List<KotlinScriptDefinition>): Boolean {
var changed = false
definitionsLock.read {
if (newDefinitions != definitions) {
@@ -42,11 +41,7 @@ class KotlinScriptDefinitionProvider {
changed = true
}
}
if (changed) {
handlersLock.read {
notificationHandlers.forEach { it() }
}
}
return changed
}
fun<TF> findScriptDefinition(file: TF): KotlinScriptDefinition? = definitionsLock.read {
@@ -55,10 +50,6 @@ class KotlinScriptDefinitionProvider {
fun<TF> isScript(file: TF): Boolean = findScriptDefinition(file) != null
fun subscribeOnDefinitionsChanged(handler: () -> Unit): Unit {
handlersLock.write { notificationHandlers.add(handler) }
}
fun addScriptDefinition(scriptDefinition: KotlinScriptDefinition) {
definitionsLock.write {
definitions.add(0, scriptDefinition)
@@ -71,6 +62,10 @@ class KotlinScriptDefinitionProvider {
}
}
fun getAllKnownFileTypes(): Iterable<LanguageFileType> = definitionsLock.read {
definitions.map { it.fileType }.distinct()
}
companion object {
@JvmStatic
fun getInstance(project: Project): KotlinScriptDefinitionProvider =
@@ -0,0 +1,107 @@
/*
* 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.script
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.project.Project
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
class KotlinScriptExternalImportsProvider(val project: Project, private val scriptDefinitionProvider: KotlinScriptDefinitionProvider) {
private val cacheLock = ReentrantReadWriteLock()
private val cache = hashMapOf<String, KotlinScriptExternalDependencies>()
private val cacheOfNulls = hashSetOf<String>()
fun <TF> getExternalImports(vararg files: TF): List<KotlinScriptExternalDependencies> = getExternalImports(files.asIterable())
fun <TF> getExternalImports(files: Iterable<TF>): List<KotlinScriptExternalDependencies> = cacheLock.read {
files.mapNotNull { file ->
val path = getFilePath(file)
cache[path]
?: if (cacheOfNulls.contains(path)) null
else scriptDefinitionProvider.findScriptDefinition(file)
?.let { it.getDependenciesFor(file, project) }
.apply { cacheLock.write {
if (this == null) {
cacheOfNulls.add(path)
}
else {
cache.put(path, this)
}
}
}
}
}
fun <TF> cacheExternalImports(files: Iterable<TF>): Unit = cacheLock.write {
val uncached = hashSetOf<String>()
files.forEach { file ->
val path = getFilePath(file)
if (!cache.containsKey(path) && !cacheOfNulls.contains(path) && !uncached.contains(path)) {
val scriptDef = scriptDefinitionProvider.findScriptDefinition(file)
if (scriptDef != null) {
val deps = scriptDef.getDependenciesFor(file, project)
if (deps != null) {
cache.put(path, deps)
}
else {
cacheOfNulls.add(path)
}
}
else {
uncached.add(path)
}
}
}
}
fun invalidateCaches() {
cacheLock.write {
cache.keys.toList().apply {
cache.clear()
}
}
}
fun <TF> invalidateCachesFor(vararg files: TF) { invalidateCachesFor(files.asIterable()) }
fun <TF> invalidateCachesFor(files: Iterable<TF>) {
cacheLock.write {
files.forEach { file ->
val path = getFilePath(file)
cache.remove(path)
cacheOfNulls.remove(path)
}
}
}
fun getKnownCombinedClasspath(): List<String> = cacheLock.read {
cache.values.flatMap { it.classpath }
}.distinct()
fun <TF> getCombinedClasspathFor(files: Iterable<TF>): List<String> =
getExternalImports(files)
.flatMap { it.classpath }
.distinct()
companion object {
@JvmStatic
fun getInstance(project: Project): KotlinScriptExternalImportsProvider? =
ServiceManager.getService(project, KotlinScriptExternalImportsProvider::class.java)
}
}
@@ -1,132 +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.script
import com.intellij.openapi.components.ServiceManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write
// TODO: replace VirtualFile usage with File for performance/unnecessary dependencies reasons
class KotlinScriptExtraImportsProvider(val project: Project, private val scriptDefinitionProvider: KotlinScriptDefinitionProvider) {
private val cacheLock = ReentrantReadWriteLock()
private val preconfigured = hashMapOf<String, List<KotlinScriptExtraImport>>()
private val cache = hashMapOf<String, List<KotlinScriptExtraImport>>()
private val envVars: Map<String, List<String>> by lazy { generateKotlinScriptClasspathEnvVars(project) }
private val notificationHandlers = ArrayList<(Iterable<String>) -> Unit>()
private val handlersLock = java.util.concurrent.locks.ReentrantReadWriteLock()
init {
val weakThis = WeakReference(this)
scriptDefinitionProvider.subscribeOnDefinitionsChanged { weakThis.get()?.invalidateAllExtraImports() }
}
fun isExtraImportsConfig(file: VirtualFile): Boolean = file.name.endsWith(IMPORTS_FILE_EXTENSION)
fun getExtraImports(vararg files: VirtualFile): List<KotlinScriptExtraImport> = getExtraImports(files.asIterable())
fun getExtraImports(files: Iterable<VirtualFile>): List<KotlinScriptExtraImport> {
val newCashedFiles = ArrayList<String>()
val res = cacheLock.read {
files.flatMap { file ->
if (file.isValid && !file.isDirectory) {
preconfigured[file.path]
?: cache[file.path]
?: scriptDefinitionProvider.findScriptDefinition(file)?.let { def ->
(listOf(KotlinScriptExtraImportFromDefinition(def)) +
(file.parent.findFileByRelativePath(file.name + IMPORTS_FILE_EXTENSION)?.let {
loadScriptExtraImportConfigs(it.inputStream).map { KotlinScriptExtraImportFromConfig(it, envVars) }
} ?: emptyList()))
.apply {
cacheLock.write { cache.put(file.path, this) }
newCashedFiles.add(file.path)
}
}
?: emptyList()
}
else emptyList()
}
}
notifyIfAny(newCashedFiles)
return res
}
fun preconfigureExtraImports(file: VirtualFile, extraImports: List<KotlinScriptExtraImport>?) {
cacheLock.write {
if (extraImports != null && extraImports.isNotEmpty()) {
preconfigured[file.path] = extraImports
}
else {
preconfigured.remove(file.path)
}
}
}
fun invalidateExtraImportsByImportsFiles(importsFiles: Iterable<VirtualFile>) {
importsFiles.mapNotNull { it.parent.findFileByRelativePath(it.name.removeSuffix(IMPORTS_FILE_EXTENSION))?.path?.let { file ->
cacheLock.write {
cache.remove(it.path)?.let { file }
}
} }.let {
notifyIfAny(it)
}
}
fun invalidateAllExtraImports() {
cacheLock.write {
cache.keys.toList().apply {
cache.clear()
}
}.let {
notifyIfAny(it)
}
}
private fun notifyIfAny(files: Iterable<String>) {
if (files.any()) {
handlersLock.read {
notificationHandlers.forEach { it(files) }
}
}
}
fun getKnownCombinedClasspath(): List<String> = cacheLock.read {
cache.values.flatMap { it.flatMap { it.classpath } }
}.distinct()
fun getCombinedClasspathFor(files: Iterable<VirtualFile>): List<String> =
getExtraImports(files)
.flatMap { it.classpath }
.distinct()
fun subscribeOnExtraImportsChanged(handler: (Iterable<String>) -> Unit): Unit {
handlersLock.write { notificationHandlers.add(handler) }
}
companion object {
@JvmStatic
fun getInstance(project: Project): KotlinScriptExtraImportsProvider? =
ServiceManager.getService(project, KotlinScriptExtraImportsProvider::class.java)
val IMPORTS_FILE_EXTENSION = ".ktsimports.xml"
}
}
@@ -26,11 +26,11 @@ fun getScriptDefinition(file: VirtualFile, project: Project): KotlinScriptDefini
fun getScriptDefinition(psiFile: PsiFile): KotlinScriptDefinition? =
KotlinScriptDefinitionProvider.getInstance(psiFile.project).findScriptDefinition(psiFile)
fun getScriptExtraImports(file: VirtualFile, project: Project): List<KotlinScriptExtraImport> =
KotlinScriptExtraImportsProvider.getInstance(project)?.getExtraImports(file) ?: emptyList()
fun getScriptExternalDependencies(file: VirtualFile, project: Project): List<KotlinScriptExternalDependencies> =
KotlinScriptExternalImportsProvider.getInstance(project)?.getExternalImports(file) ?: emptyList()
fun getScriptExtraImports(psiFile: PsiFile): List<KotlinScriptExtraImport> =
fun getScriptExternalDependencies(psiFile: PsiFile): List<KotlinScriptExternalDependencies> =
psiFile.virtualFile?.let { file ->
KotlinScriptExtraImportsProvider.getInstance(psiFile.project)?.getExtraImports(file)
KotlinScriptExternalImportsProvider.getInstance(psiFile.project)?.getExternalImports(file)
} ?: emptyList()
@@ -52,7 +52,8 @@ object SimpleUntypedAst {
convert(expression.entries[0])
else
SimpleUntypedAst.Node.str(name, "")
// convertStringTemplateExpression(expression, parent, expression.entries.size - 1)
// TODO: parse expressions, etc. e.g.:
// convertStringTemplateExpression(expression, parent, expression.entries.size - 1)
}
else -> Node.empty(name)
@@ -72,3 +73,9 @@ object SimpleUntypedAst {
}
}
fun parseAnnotation(ann: KtAnnotationEntry): SimpleUntypedAst.Node.list<Any> {
val wann = SimpleUntypedAst.KtAnnotationWrapper(ann)
val vals = wann.valueArguments
return SimpleUntypedAst.Node.list(wann.name, vals)
}
@@ -0,0 +1,69 @@
/*
* 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.script
import com.intellij.openapi.util.JDOMUtil
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.util.xmlb.XmlSerializer
import com.intellij.util.xmlb.annotations.AbstractCollection
import com.intellij.util.xmlb.annotations.Tag
import java.io.File
import java.io.InputStream
import java.util.*
@Tag("import")
class KotlinScriptExternalDependenciesConfig : KotlinScriptExternalDependencies {
@Tag("classpath")
@AbstractCollection(surroundWithTag = false, elementTag = "path", elementValueAttribute = "")
override var classpath: MutableList<String> = ArrayList()
@Tag("imports")
@AbstractCollection(surroundWithTag = false, elementTag = "name", elementValueAttribute = "")
override var imports: MutableList<String> = ArrayList()
@Tag("sources")
@AbstractCollection(surroundWithTag = false, elementTag = "path", elementValueAttribute = "")
override var sources: MutableList<String> = ArrayList()
}
fun loadScriptExternalImportConfigs(configFile: File): List<KotlinScriptExternalDependenciesConfig> =
JDOMUtil.loadDocument(configFile).rootElement.children.mapNotNull {
XmlSerializer.deserialize(it, KotlinScriptExternalDependenciesConfig::class.java)
}
fun loadScriptExternalImportConfigs(configStream: InputStream): List<KotlinScriptExternalDependenciesConfig> =
JDOMUtil.loadDocument(configStream).rootElement.children.mapNotNull {
XmlSerializer.deserialize(it, KotlinScriptExternalDependenciesConfig::class.java)
}
fun <TF> getScriptDependenciesFromConfig(file: TF): KotlinScriptExternalDependencies? {
val IMPORTS_FILE_EXTENSION = ".ktsimports.xml"
fun streamFromSibling(file: VirtualFile): InputStream? =
file.parent.findFileByRelativePath(file.name + IMPORTS_FILE_EXTENSION)?.let { it.inputStream }
fun streamFromSibling(file: File): InputStream? {
val sibling = File(file.parentFile, file.name + IMPORTS_FILE_EXTENSION)
return if (sibling.exists()) sibling.inputStream()
else null
}
return when (file) {
is VirtualFile -> streamFromSibling(file)
is PsiFile -> streamFromSibling(file.originalFile.virtualFile)
is File -> streamFromSibling(file)
else -> throw IllegalArgumentException("Unsupported file type $file")
}?.let { KotlinScriptExternalDependenciesUnion(loadScriptExternalImportConfigs(it)) }
}
@@ -1,67 +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.script
import com.intellij.openapi.util.JDOMUtil
import com.intellij.util.xmlb.XmlSerializer
import com.intellij.util.xmlb.annotations.AbstractCollection
import com.intellij.util.xmlb.annotations.Tag
import java.io.File
import java.io.InputStream
import java.util.*
/**
* A particular script dependency, additional to script kind dependencies defined by KotlinScriptDefinition
*/
interface KotlinScriptExtraImport {
val classpath: List<String>
val names: List<String>
}
// -----
@Tag("import")
class KotlinScriptExtraImportConfig {
@Tag("classpath")
@AbstractCollection(surroundWithTag = false, elementTag = "path", elementValueAttribute = "")
var classpath: MutableList<String> = ArrayList()
@Tag("names")
@AbstractCollection(surroundWithTag = false, elementTag = "name", elementValueAttribute = "")
var names: MutableList<String> = ArrayList()
}
fun loadScriptExtraImportConfigs(configFile: File): List<KotlinScriptExtraImportConfig> =
JDOMUtil.loadDocument(configFile).rootElement.children.mapNotNull {
XmlSerializer.deserialize(it, KotlinScriptExtraImportConfig::class.java)
}
fun loadScriptExtraImportConfigs(configStream: InputStream): List<KotlinScriptExtraImportConfig> =
JDOMUtil.loadDocument(configStream).rootElement.children.mapNotNull {
XmlSerializer.deserialize(it, KotlinScriptExtraImportConfig::class.java)
}
class KotlinScriptExtraImportFromConfig(val config : KotlinScriptExtraImportConfig, val envVars: Map<String, List<String>>) : KotlinScriptExtraImport {
override val classpath: List<String> by lazy { config.classpath.evalWithVars(envVars).distinct() }
override val names: List<String>
get() = config.names
}
class KotlinScriptExtraImportFromDefinition(val scriptDefinition: KotlinScriptDefinition) : KotlinScriptExtraImport {
override val classpath: List<String> get() = scriptDefinition.getScriptDependenciesClasspath()
override val names: List<String> = emptyList()
}
@@ -16,33 +16,33 @@
package org.jetbrains.kotlin.script
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.StandardFileSystems
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiManager
import org.jetbrains.kotlin.descriptors.ScriptDescriptor
import org.jetbrains.kotlin.name.Name
import org.jetbrains.kotlin.parsing.KotlinParserDefinition
import org.jetbrains.kotlin.psi.KtAnnotation
import org.jetbrains.kotlin.psi.KtAnnotationEntry
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtScript
import org.jetbrains.kotlin.types.KotlinType
import java.io.File
import kotlin.reflect.KClass
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class ScriptFilePattern(val pattern: String)
interface ScriptDependencies {
val classpath: List<String>
val implicitImports: List<String>
}
interface GetScriptDependencies {
operator fun invoke(annotations: Iterable<KtAnnotationEntry>, context: Any?): ScriptDependencies? = null
operator fun invoke(context: Any?): ScriptDependencies? = null
operator fun invoke(annotations: Iterable<KtAnnotationEntry>, context: Any?): KotlinScriptExternalDependencies? = null
operator fun invoke(context: Any?): KotlinScriptExternalDependencies? = null
}
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
annotation class ScriptDependencyExtractor(val extractor: KClass<out GetScriptDependencies>)
annotation class ScriptDependencyResolver(val extractor: KClass<out GetScriptDependencies>)
data class KotlinScriptDefinitionFromTemplate(val template: KClass<out Any>, val context: Any?) : KotlinScriptDefinition {
override val name = template.simpleName!!
@@ -63,13 +63,39 @@ data class KotlinScriptDefinitionFromTemplate(val template: KClass<out Any>, val
override fun getScriptName(script: KtScript): Name = ScriptNameUtil.fileNameWithExtensionStripped(script, KotlinParserDefinition.STD_SCRIPT_EXT)
private val dependenciesExtractors by lazy {
template.annotations.mapNotNull { it as? ScriptDependencyExtractor }.map { it.extractor.constructors.first().call() }
template.annotations.mapNotNull { it as? ScriptDependencyResolver }.map { it.extractor.constructors.first().call() }
}
private val dependencies by lazy {
private val dependencies: List<KotlinScriptExternalDependencies> by lazy {
dependenciesExtractors.mapNotNull { it(context) }
}
override fun getScriptDependenciesClasspath(): List<String> = dependencies.flatMap { it.classpath }
override fun <TF> getDependenciesFor(file: TF, project: Project): KotlinScriptExternalDependencies? {
val fileAnnotations = getAnnotationEntries(file, project)
val fileDeps = dependenciesExtractors.mapNotNull { it(fileAnnotations, context) }
return KotlinScriptExternalDependenciesUnion(dependencies + fileDeps)
}
private fun <TF> getAnnotationEntries(file: TF, project: Project): Iterable<KtAnnotationEntry> = when (file) {
is PsiFile -> getAnnotationEntriesFromPsiFile(file)
is VirtualFile -> getAnnotationEntriesFromVirtualFile(file, project)
is File -> {
val virtualFile = (StandardFileSystems.local().findFileByPath(file.absolutePath)
?: throw java.lang.IllegalArgumentException("Unable to find file ${file.canonicalPath}"))
getAnnotationEntriesFromVirtualFile(virtualFile, project)
}
else -> throw IllegalArgumentException("Unsupported file type $file")
}
private fun getAnnotationEntriesFromPsiFile(file: PsiFile) =
if (file is KtFile) file.annotationEntries
else throw IllegalArgumentException("Unable to extract kotlin annotations from ${file.name} (${file.fileType})")
private fun getAnnotationEntriesFromVirtualFile(file: VirtualFile, project: Project): Iterable<KtAnnotationEntry> {
val psifile: PsiFile = PsiManager.getInstance(project).findFile(file)
?: throw java.lang.IllegalArgumentException("Unable to load PSI from ${file.canonicalPath}")
return getAnnotationEntriesFromPsiFile(psifile)
}
}