From 2219b950f15254d3d06e65f034bd54151459ded1 Mon Sep 17 00:00:00 2001 From: Ilya Chernikov Date: Mon, 30 Sep 2019 15:50:43 +0200 Subject: [PATCH] Implement resolve top-level functions and props from classloader #KT-33892 fixed --- build.gradle.kts | 1 + .../jvm/compiler/JvmPackagePartProvider.kt | 101 +++++------- .../load/kotlin/JvmPackagePartProviderBase.kt | 54 +++++++ .../runtime/components/RuntimeModuleData.kt | 5 +- .../jvmhost/test/ResolveDependenciesTest.kt | 3 +- libraries/scripting/jvm/build.gradle.kts | 3 +- .../jvm/util/jvmClassLoaderUtil.kt | 146 ++++++++++++++++++ .../experimental/jvm/util/jvmClasspathUtil.kt | 30 ++-- .../script/experimental/jvm/test/utilsTest.kt | 126 +++++++++++++++ libraries/scripting/jvm/testData/testJar.jar | Bin 0 -> 1003 bytes .../PackageFragmentFromClassLoaderProvider.kt | 22 ++- .../PackagePartFromClassLoaderProvider.kt | 37 +++++ .../plugin/impl/ScriptJvmCompilerImpls.kt | 26 +++- 13 files changed, 461 insertions(+), 93 deletions(-) create mode 100644 core/descriptors.jvm/src/org/jetbrains/kotlin/load/kotlin/JvmPackagePartProviderBase.kt create mode 100644 libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClassLoaderUtil.kt create mode 100644 libraries/scripting/jvm/test/kotlin/script/experimental/jvm/test/utilsTest.kt create mode 100644 libraries/scripting/jvm/testData/testJar.jar create mode 100644 plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/PackagePartFromClassLoaderProvider.kt diff --git a/build.gradle.kts b/build.gradle.kts index 24932a1a038..e58a8290508 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -534,6 +534,7 @@ tasks { dependsOn(":kotlin-script-util:test") dependsOn(":kotlin-scripting-compiler:test") dependsOn(":kotlin-scripting-common:test") + dependsOn(":kotlin-scripting-jvm:test") dependsOn(":kotlin-scripting-jvm-host-test:test") dependsOn(":kotlin-scripting-jsr223-test:test") dependsOn(":kotlin-scripting-jvm-host-test:embeddableTest") diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/JvmPackagePartProvider.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/JvmPackagePartProvider.kt index df829622b42..26d1e453bc4 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/JvmPackagePartProvider.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/JvmPackagePartProvider.kt @@ -25,14 +25,11 @@ import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity.LOGGING import org.jetbrains.kotlin.cli.common.messages.MessageCollector import org.jetbrains.kotlin.cli.jvm.index.JavaRoot import org.jetbrains.kotlin.config.LanguageVersionSettings -import org.jetbrains.kotlin.load.kotlin.PackagePartProvider +import org.jetbrains.kotlin.load.kotlin.JvmPackagePartProviderBase import org.jetbrains.kotlin.load.kotlin.loadModuleMapping import org.jetbrains.kotlin.metadata.jvm.deserialization.JvmMetadataVersion import org.jetbrains.kotlin.metadata.jvm.deserialization.ModuleMapping -import org.jetbrains.kotlin.metadata.jvm.deserialization.PackageParts -import org.jetbrains.kotlin.name.ClassId import org.jetbrains.kotlin.resolve.CompilerDeserializationConfiguration -import org.jetbrains.kotlin.serialization.deserialization.MetadataPartProvider import java.io.ByteArrayOutputStream import java.io.EOFException import java.io.PrintStream @@ -40,49 +37,10 @@ import java.io.PrintStream class JvmPackagePartProvider( languageVersionSettings: LanguageVersionSettings, private val scope: GlobalSearchScope -) : PackagePartProvider, MetadataPartProvider { - private data class ModuleMappingInfo(val root: VirtualFile, val mapping: ModuleMapping, val name: String) - +) : JvmPackagePartProviderBase() { private val deserializationConfiguration = CompilerDeserializationConfiguration(languageVersionSettings) - private val loadedModules: MutableList = SmartList() - - override fun findPackageParts(packageFqName: String): List { - val rootToPackageParts = getPackageParts(packageFqName) - if (rootToPackageParts.isEmpty()) return emptyList() - - val result = linkedSetOf() - val visitedMultifileFacades = linkedSetOf() - for ((_, packageParts) in rootToPackageParts) { - for (name in packageParts.parts) { - val facadeName = packageParts.getMultifileFacadeName(name) - if (facadeName == null || facadeName !in visitedMultifileFacades) { - result.add(name) - } - } - packageParts.parts.mapNotNullTo(visitedMultifileFacades, packageParts::getMultifileFacadeName) - } - return result.toList() - } - - override fun findMetadataPackageParts(packageFqName: String): List = - getPackageParts(packageFqName).values.flatMap(PackageParts::metadataParts).distinct() - - @Synchronized - private fun getPackageParts(packageFqName: String): Map { - val result = mutableMapOf() - for ((root, mapping) in loadedModules) { - val newParts = mapping.findPackageParts(packageFqName) ?: continue - result[root]?.let { parts -> parts += newParts } ?: result.put(root, newParts) - } - return result - } - - override fun getAnnotationsOnBinaryModule(moduleName: String): List { - return loadedModules.mapNotNull { (_, mapping, name) -> - if (name == moduleName) mapping.moduleData.annotations.map(ClassId::fromString) else null - }.flatten() - } + override val loadedModules: MutableList> = SmartList() fun addRoots(roots: List, messageCollector: MessageCollector) { for ((root, type) in roots) { @@ -93,29 +51,40 @@ class JvmPackagePartProvider( for (moduleFile in metaInf.children) { if (!moduleFile.name.endsWith(ModuleMapping.MAPPING_FILE_EXT)) continue - try { - val mapping = ModuleMapping.loadModuleMapping( - moduleFile.contentsToByteArray(), moduleFile.toString(), deserializationConfiguration - ) { incompatibleVersion -> - messageCollector.report( - ERROR, - "Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is " + - "$incompatibleVersion, expected version is ${JvmMetadataVersion.INSTANCE}.", - CompilerMessageLocation.create(moduleFile.path) - ) - } - loadedModules.add(ModuleMappingInfo(root, mapping, moduleFile.nameWithoutExtension)) - } catch (e: EOFException) { - messageCollector.report( - ERROR, "Error occurred when reading the module: ${e.message}", CompilerMessageLocation.create(moduleFile.path) - ) - messageCollector.report( - LOGGING, - String(ByteArrayOutputStream().also { e.printStackTrace(PrintStream(it)) }.toByteArray()), - CompilerMessageLocation.create(moduleFile.path) - ) + tryLoadModuleMapping( + { moduleFile.contentsToByteArray() }, moduleFile.toString(), moduleFile.path, + deserializationConfiguration, messageCollector + )?.let { + loadedModules.add(ModuleMappingInfo(root, it, moduleFile.nameWithoutExtension)) } } } } } + +fun tryLoadModuleMapping( + getModuleBytes: () -> ByteArray, + debugName: String, + modulePath: String, + deserializationConfiguration: CompilerDeserializationConfiguration, + messageCollector: MessageCollector +): ModuleMapping? = try { + ModuleMapping.loadModuleMapping(getModuleBytes(), debugName, deserializationConfiguration) { incompatibleVersion -> + messageCollector.report( + ERROR, + "Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is " + + "$incompatibleVersion, expected version is ${JvmMetadataVersion.INSTANCE}.", + CompilerMessageLocation.create(modulePath) + ) + } +} catch (e: EOFException) { + messageCollector.report( + ERROR, "Error occurred when reading the module: ${e.message}", CompilerMessageLocation.create(modulePath) + ) + messageCollector.report( + LOGGING, + String(ByteArrayOutputStream().also { e.printStackTrace(PrintStream(it)) }.toByteArray()), + CompilerMessageLocation.create(modulePath) + ) + null +} diff --git a/core/descriptors.jvm/src/org/jetbrains/kotlin/load/kotlin/JvmPackagePartProviderBase.kt b/core/descriptors.jvm/src/org/jetbrains/kotlin/load/kotlin/JvmPackagePartProviderBase.kt new file mode 100644 index 00000000000..76ea64ef933 --- /dev/null +++ b/core/descriptors.jvm/src/org/jetbrains/kotlin/load/kotlin/JvmPackagePartProviderBase.kt @@ -0,0 +1,54 @@ +/* + * 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.load.kotlin + +import org.jetbrains.kotlin.metadata.jvm.deserialization.ModuleMapping +import org.jetbrains.kotlin.metadata.jvm.deserialization.PackageParts +import org.jetbrains.kotlin.name.ClassId +import org.jetbrains.kotlin.serialization.deserialization.MetadataPartProvider + +abstract class JvmPackagePartProviderBase : PackagePartProvider, MetadataPartProvider { + + protected data class ModuleMappingInfo(val key: MappingsKey, val mapping: ModuleMapping, val name: String) + + protected abstract val loadedModules: MutableList> + + override fun findPackageParts(packageFqName: String): List { + val rootToPackageParts: Collection = getPackageParts(packageFqName) + if (rootToPackageParts.isEmpty()) return emptyList() + + val result = linkedSetOf() + val visitedMultifileFacades = linkedSetOf() + for (packageParts in rootToPackageParts) { + for (name in packageParts.parts) { + val facadeName = packageParts.getMultifileFacadeName(name) + if (facadeName == null || facadeName !in visitedMultifileFacades) { + result.add(name) + } + } + packageParts.parts.mapNotNullTo(visitedMultifileFacades, packageParts::getMultifileFacadeName) + } + return result.toList() + } + + override fun findMetadataPackageParts(packageFqName: String): List = + getPackageParts(packageFqName).flatMap(PackageParts::metadataParts).distinct() + + private fun getPackageParts(packageFqName: String): Collection { + val result = mutableMapOf() + for ((root, mapping) in loadedModules) { + val newParts = mapping.findPackageParts(packageFqName) ?: continue + result[root]?.let { parts -> parts += newParts } ?: result.put(root, newParts) + } + return result.values + } + + override fun getAnnotationsOnBinaryModule(moduleName: String): List { + return loadedModules.mapNotNull { (_, mapping, name) -> + if (name == moduleName) mapping.moduleData.annotations.map(ClassId::fromString) else null + }.flatten() + } +} diff --git a/core/descriptors.runtime/src/org/jetbrains/kotlin/descriptors/runtime/components/RuntimeModuleData.kt b/core/descriptors.runtime/src/org/jetbrains/kotlin/descriptors/runtime/components/RuntimeModuleData.kt index 4ce4fd428ff..20d8877bbaa 100644 --- a/core/descriptors.runtime/src/org/jetbrains/kotlin/descriptors/runtime/components/RuntimeModuleData.kt +++ b/core/descriptors.runtime/src/org/jetbrains/kotlin/descriptors/runtime/components/RuntimeModuleData.kt @@ -106,14 +106,15 @@ fun makeLazyJavaPackageFragmentFromClassLoaderProvider( notFoundClasses: NotFoundClasses, reflectKotlinClassFinder: KotlinClassFinder, deserializedDescriptorResolver: DeserializedDescriptorResolver, - singleModuleClassResolver: ModuleClassResolver + singleModuleClassResolver: ModuleClassResolver, + packagePartProvider: PackagePartProvider = PackagePartProvider.Empty ): LazyJavaPackageFragmentProvider { val annotationTypeQualifierResolver = AnnotationTypeQualifierResolver(storageManager, Jsr305State.DISABLED) val javaResolverComponents = JavaResolverComponents( storageManager, ReflectJavaClassFinder(classLoader), reflectKotlinClassFinder, deserializedDescriptorResolver, SignaturePropagator.DO_NOTHING, RuntimeErrorReporter, JavaResolverCache.EMPTY, JavaPropertyInitializerEvaluator.DoNothing, SamConversionResolver.Empty, RuntimeSourceElementFactory, - singleModuleClassResolver, PackagePartProvider.Empty, SupertypeLoopChecker.EMPTY, LookupTracker.DO_NOTHING, module, + singleModuleClassResolver, packagePartProvider, SupertypeLoopChecker.EMPTY, LookupTracker.DO_NOTHING, module, ReflectionTypes(module, notFoundClasses), annotationTypeQualifierResolver, SignatureEnhancement(annotationTypeQualifierResolver, Jsr305State.DISABLED), JavaClassesTracker.Default, JavaResolverSettings.Default, NewKotlinTypeChecker.Default diff --git a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ResolveDependenciesTest.kt b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ResolveDependenciesTest.kt index ac03e8d4749..5758950565f 100644 --- a/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ResolveDependenciesTest.kt +++ b/libraries/scripting/jvm-host-test/test/kotlin/script/experimental/jvmhost/test/ResolveDependenciesTest.kt @@ -52,10 +52,9 @@ class ResolveDependenciesTest : TestCase() { runScriptAndCheckResult(classImportScript, configurationWithDependenciesFromClasspath, null, 42) } - @Ignore @Test // This doesn't work since there is no way to resolve a top-level function/property via reflection now (see #KT-33892) - fun ignore_testResolveFunAndValFromClassloader() { + fun testResolveFunAndValFromClassloader() { runScriptAndCheckResult(funAndValAccessScript, configurationWithDependenciesFromClassloader, null, 42) runScriptAndCheckResult(funAndValImportScript, configurationWithDependenciesFromClassloader, null, 42) } diff --git a/libraries/scripting/jvm/build.gradle.kts b/libraries/scripting/jvm/build.gradle.kts index da391d74f65..807f3003b22 100644 --- a/libraries/scripting/jvm/build.gradle.kts +++ b/libraries/scripting/jvm/build.gradle.kts @@ -10,11 +10,12 @@ dependencies { compile(project(":kotlin-script-runtime")) compile(kotlinStdlib()) compile(project(":kotlin-scripting-common")) + testCompile(commonDep("junit")) } sourceSets { "main" { projectDefault() } - "test" {} + "test" { projectDefault() } } publish() diff --git a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClassLoaderUtil.kt b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClassLoaderUtil.kt new file mode 100644 index 00000000000..129ea060a0b --- /dev/null +++ b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClassLoaderUtil.kt @@ -0,0 +1,146 @@ +/* + * 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 kotlin.script.experimental.jvm.util + +import java.io.File +import java.io.FileInputStream +import java.io.IOException +import java.io.InputStream +import java.net.JarURLConnection +import java.net.URL +import java.util.jar.JarFile +import java.util.jar.JarInputStream +import kotlin.script.experimental.jvm.impl.toFileOrNull + +fun ClassLoader.forAllMatchingFiles(namePattern: String, body: (String, InputStream) -> Unit) { + val processedDirs = HashSet() + val processedJars = HashSet() + val nameRegex = namePatternToRegex(namePattern) + + fun iterateResources(vararg keyResourcePaths: String) { + for (keyResourcePath in keyResourcePaths) { + val resourceRootCalc = ClassLoaderResourceRootFIlePathCalculator(keyResourcePath) + for (url in getResources(keyResourcePath)) { + if (url.protocol == "jar") { + val jarConnection = url.openConnection() as? JarURLConnection + val jarUrl = jarConnection?.jarFileURL + if (jarUrl != null && !processedJars.contains(jarUrl)) { + processedJars.add(jarUrl) + try { + jarConnection.jarFile + } catch (_: IOException) { + // TODO: consider error reporting + null + }?.let { + forAllMatchingFilesInJarFile(it, nameRegex, body) + } + } + } else { + val rootDir = url.toFileOrNull()?.let { resourceRootCalc(it) } + if (rootDir != null && rootDir.isDirectory && !processedDirs.contains(rootDir)) { + processedDirs.add(rootDir) + forAllMatchingFilesInDirectory(rootDir, namePattern, body) + } + } + } + } + } + + iterateResources("", JAR_MANIFEST_RESOURCE_NAME) +} + +internal val wildcardChars = "*?".toCharArray() +internal val patternCharsToEscape = ".*?+()[]^\${}|".toCharArray().also { assert(wildcardChars.all { wc -> it.contains(wc) }) } + +private fun Char.escape(): String = (if (patternCharsToEscape.contains(this)) "\\" else "") + this + +internal val pathSeparatorChars = "/".let { if (File.separatorChar == '/') it else it + File.separator }.toCharArray() +internal val pathElementPattern = if (File.separatorChar == '/') "[^/]*" else "[^/${File.separatorChar.escape()}]*" +internal val pathSeparatorPattern = if (File.separatorChar == '/') "/" else "[/${File.separatorChar.escape()}]." +internal val specialPatternChars = patternCharsToEscape + pathSeparatorChars + +internal fun forAllMatchingFilesInDirectory(baseDir: File, namePattern: String, body: (String, InputStream) -> Unit) { + val patternStart = namePattern.indexOfAny(wildcardChars) + if (patternStart < 0) { + // assuming a single file + baseDir.resolve(namePattern).takeIf { it.exists() && it.isFile }?.let { file -> + body(file.relativeToOrSelf(baseDir).path, file.inputStream()) + } + } else { + val patternDirStart = namePattern.lastIndexOfAny(pathSeparatorChars, patternStart) + val root = if (patternDirStart <= 0) baseDir else baseDir.resolve(namePattern.substring(0, patternDirStart)) + if (root.exists() && root.isDirectory) { + val re = namePatternToRegex(namePattern.substring(patternDirStart + 1)) + root.walkTopDown().filter { + re.matches(it.relativeToOrSelf(root).path) + }.forEach { file -> + body(file.relativeToOrSelf(baseDir).path, file.inputStream()) + } + } + } +} + +internal fun forAllMatchingFilesInJarStream(jarInputStream: JarInputStream, nameRegex: Regex, body: (String, InputStream) -> Unit) { + do { + val entry = jarInputStream.nextJarEntry + if (entry != null) { + try { + if (!entry.isDirectory && nameRegex.matches(entry.name)) { + body(entry.name, jarInputStream) + } + } finally { + jarInputStream.closeEntry() + } + } + } while (entry != null) +} + +internal fun forAllMatchingFilesInJar(jarFile: File, nameRegex: Regex, body: (String, InputStream) -> Unit) { + JarInputStream(FileInputStream(jarFile)).use { + forAllMatchingFilesInJarStream(it, nameRegex, body) + } +} + +internal fun forAllMatchingFilesInJarFile(jarFile: JarFile, nameRegex: Regex, body: (String, InputStream) -> Unit) { + jarFile.entries().asSequence().forEach { entry -> + if (!entry.isDirectory && nameRegex.matches(entry.name)) { + jarFile.getInputStream(entry).use { stream -> + body(entry.name, stream) + } + } + } +} + +internal fun namePatternToRegex(pattern: String): Regex = Regex( + buildString { + var current = 0 + loop@ while (current < pattern.length) { + val nextIndex = pattern.indexOfAny(specialPatternChars, current) + val next = if (nextIndex < 0) pattern.length else nextIndex + append(pattern.substring(current, next)) + current = next + 1 + when { + next >= pattern.length -> break@loop + + pathSeparatorChars.contains(pattern[next]) -> append(pathSeparatorPattern) + + pattern[next] == '?' -> append('.') + + pattern[next] == '*' && next + 1 < pattern.length && pattern[next + 1] == '*' -> { + append(".*") + current++ + } + + pattern[next] == '*' -> append(pathElementPattern) + + else -> { + append('\\') + append(pattern[next]) + } + } + } + } +) \ No newline at end of file diff --git a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClasspathUtil.kt b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClasspathUtil.kt index fd61e3dea6c..25c49129c84 100644 --- a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClasspathUtil.kt +++ b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClasspathUtil.kt @@ -39,7 +39,7 @@ internal const val KOTLIN_COMPILER_JAR = "$KOTLIN_COMPILER_NAME.jar" private val JAR_COLLECTIONS_CLASSES_PATHS = arrayOf("BOOT-INF/classes", "WEB-INF/classes") private val JAR_COLLECTIONS_LIB_PATHS = arrayOf("BOOT-INF/lib", "WEB-INF/lib") private val JAR_COLLECTIONS_KEY_PATHS = JAR_COLLECTIONS_CLASSES_PATHS + JAR_COLLECTIONS_LIB_PATHS -private const val JAR_MANIFEST_RESOURCE_NAME = "META-INF/MANIFEST.MF" +internal const val JAR_MANIFEST_RESOURCE_NAME = "META-INF/MANIFEST.MF" internal const val KOTLIN_SCRIPT_CLASSPATH_PROPERTY = "kotlin.script.classpath" internal const val KOTLIN_COMPILER_CLASSPATH_PROPERTY = "kotlin.compiler.classpath" @@ -108,20 +108,28 @@ private fun ClassLoader.classPathFromGetUrlsMethodOrNull(): Sequence? { } } +internal class ClassLoaderResourceRootFIlePathCalculator(private val keyResourcePath: String) { + private var keyResourcePathDepth = -1 + + operator fun invoke(resourceFile: File): File { + if (keyResourcePathDepth < 0) { + keyResourcePathDepth = if (keyResourcePath.isBlank()) 0 else (keyResourcePath.trim('/').count { it == '/' } + 1) + } + var root = resourceFile + for (i in 0 until keyResourcePathDepth) { + root = root.parentFile + } + return root + } +} + internal fun ClassLoader.rawClassPathFromKeyResourcePath(keyResourcePath: String): Sequence { - var keyResourcePathDepth = -1 + val resourceRootCalc = ClassLoaderResourceRootFIlePathCalculator(keyResourcePath) return getResources(keyResourcePath).asSequence().mapNotNull { url -> if (url.protocol == "jar") { (url.openConnection() as? JarURLConnection)?.jarFileURL?.toFileOrNull() - } else url.toFileOrNull()?.let { file -> - if (keyResourcePathDepth < 0) { - keyResourcePathDepth = if (keyResourcePath.isBlank()) 0 else (keyResourcePath.trim('/').count { it == '/' } + 1) - } - var root = file - for (i in 0 until keyResourcePathDepth) { - root = root.parentFile - } - root + } else { + url.toFileOrNull()?.let { resourceRootCalc(it) } } } } diff --git a/libraries/scripting/jvm/test/kotlin/script/experimental/jvm/test/utilsTest.kt b/libraries/scripting/jvm/test/kotlin/script/experimental/jvm/test/utilsTest.kt new file mode 100644 index 00000000000..2302f3bf40e --- /dev/null +++ b/libraries/scripting/jvm/test/kotlin/script/experimental/jvm/test/utilsTest.kt @@ -0,0 +1,126 @@ +/* + * 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 kotlin.script.experimental.jvm.test + +import junit.framework.TestCase +import org.junit.Test +import java.io.File +import java.util.jar.JarFile +import java.util.jar.JarInputStream +import kotlin.script.experimental.jvm.util.* + +class UtilsTest : TestCase() { + + @Test + fun testPatternConversionWildcards() { + assertPattern("a${pathSeparatorPattern}b\\.$pathElementPattern", "a/b.*") + assertPattern("a$pathSeparatorPattern$pathElementPattern\\.txt", "a/*.txt") + assertPattern("a$pathSeparatorPattern.*/b", "a/**/b") + assertPattern("a${pathSeparatorPattern}b.\\.txt", "a/b?.txt") + assertPattern("$pathElementPattern/b\\.txt", "*/b.txt") + assertPattern(".*${pathSeparatorPattern}b\\.txt", "**/b.txt") + } + + @Test + fun testPatternConversionEscaping() { + assertPattern("aa\\+\\(\\)\\[\\]\\^\\\$\\{\\}\\|", "aa+()[]^\${}|") + assertPattern("\\+\\(\\)\\[\\]\\^\\\$\\{\\}\\|bb", "+()[]^\${}|bb") + } + + @Test + fun testSelectFilesInDir() { + + val rootDir = File(".") + + fun assertProjectFilesBy(pattern: String, vararg paths: String) { + val res = ArrayList>() + + forAllMatchingFilesInDirectory(rootDir, pattern) { path, stream -> + res.add(path to stream.reader().readText()) + } + assertEquals(paths.toSet(), res.mapTo(HashSet()) { it.first }) + + res.forEach { (path, bytes) -> + val data = File(path).readText() + assertEquals("Mismatching data for $path", data, bytes) + } + } + + assertProjectFilesBy("*.kt") // none + assertProjectFilesBy("**/sss/*.kt") // none + assertProjectFilesBy( + "src/kotlin/script/experimental/jvm/util/jvmClassLoaderUtil.kt", + "src/kotlin/script/experimental/jvm/util/jvmClassLoaderUtil.kt" + ) + assertProjectFilesBy( + "src/kotlin/script/experimental/jvm/util/jvm?lassLoaderUtil.kt", + "src/kotlin/script/experimental/jvm/util/jvmClassLoaderUtil.kt" + ) + assertProjectFilesBy( + "src/kotlin/script/experimental/jvm/util/jvm*LoaderUtil.kt", + "src/kotlin/script/experimental/jvm/util/jvmClassLoaderUtil.kt" + ) + assertProjectFilesBy("**/jvmClassLoaderUtil.kt", "src/kotlin/script/experimental/jvm/util/jvmClassLoaderUtil.kt") + assertProjectFilesBy("**/script/**/jvmClassLoaderUtil.kt", "src/kotlin/script/experimental/jvm/util/jvmClassLoaderUtil.kt") + assertProjectFilesBy("src/**/jvmClassLoaderUtil.kt", "src/kotlin/script/experimental/jvm/util/jvmClassLoaderUtil.kt") + assertProjectFilesBy("test/**/?????Test.*", "test/kotlin/script/experimental/jvm/test/utilsTest.kt") + + val allSrcKtFiles = HashSet() + forAllMatchingFilesInDirectory(rootDir, "src/**/*.kt") { path, _ -> + allSrcKtFiles.add(path) + } + val allExpectedSrcKtFiles = + rootDir.walkTopDown().filter { + it.relativeToOrSelf(rootDir).path.startsWith("src") && it.extension == "kt" + }.mapTo(HashSet()) { + it.relativeToOrSelf(rootDir).path + } + assertEquals(allExpectedSrcKtFiles, allSrcKtFiles) + } + + @Test + fun testSelectFilesInJar() { + + fun JarFile.filesBy(pattern: String): Map { + val res = HashMap() + forAllMatchingFilesInJarFile(this, namePatternToRegex(pattern)) { path, stream -> + res[path] = stream.reader().readText().trim() + } + return res + } + + fun JarInputStream.filesBy(pattern: String): Map { + val res = HashMap() + forAllMatchingFilesInJarStream(this, namePatternToRegex(pattern)) { path, stream -> + res[path] = stream.reader().readText().trim() + } + return res + } + + fun assertFiles(actual: Map, vararg expected: Pair) { + val expectedAsMap = expected.toMap() + assertEquals(expectedAsMap, actual) + } + + fun assertMatchingFilesInJarTwoWay(jar: File, pattern: String, vararg expected: Pair) { + assertFiles( JarFile(jar).filesBy(pattern), *expected) + assertFiles( JarInputStream(jar.inputStream()).use { it.filesBy(pattern) }, *expected) + } + + val jar = File("testData/testJar.jar") + assertTrue(jar.exists()) + + assertMatchingFilesInJarTwoWay(jar, "META-INF/*.kotlin_module", "META-INF/abc.kotlin_module" to "module") + assertMatchingFilesInJarTwoWay(jar, "META-INF/*.kotlin") // none + assertMatchingFilesInJarTwoWay(jar, "**/*.class", "a/b/c/d1.class" to "d1", "a/b/c/d1\$s1.class" to "d1s1") + assertMatchingFilesInJarTwoWay(jar, "**/*\$*.class", "a/b/c/d1\$s1.class" to "d1s1") + } + + private fun assertPattern(expected: String, pattern: String) { + assertEquals(expected, namePatternToRegex(pattern).pattern) + } + +} diff --git a/libraries/scripting/jvm/testData/testJar.jar b/libraries/scripting/jvm/testData/testJar.jar new file mode 100644 index 0000000000000000000000000000000000000000..69ad3dd1adbdf3af49a0a828febe654f54a7aa7d GIT binary patch literal 1003 zcmWIWW@Zs#;Nak3SlQw3&wvCt8CV#6T|*poJ^kGD|D9rBU}gyLX6FE@V1gp1C+dSN z#4gVQmQO-bu>>tl*`O+t!D=Ak9^H)UY(co16hoC_L%rmj#NuLaLmjgx5VwA8w3q~P zCo2#mJdbV@ACf6BBcKL2g@{iBX=Vl+z{n)RfSLecu?tE7r~n?LpyUwXjj9znQb5s< z0JcCTTq{yyKsEssdC0K() { + private val deserializationConfiguration = CompilerDeserializationConfiguration(languageVersionSettings) + + override val loadedModules: MutableList> = SmartList() + + init { + classLoader.forAllMatchingFiles("META-INF/*.${ModuleMapping.MAPPING_FILE_EXT}") { name, stream -> + tryLoadModuleMapping( + { stream.readBytes() }, name, name, deserializationConfiguration, messageCollector + )?.let { + val moduleName = name.removePrefix("META-INF/").removeSuffix(".${ModuleMapping.MAPPING_FILE_EXT}") + loadedModules.add(ModuleMappingInfo(name, it, moduleName)) + } + } + } +} + diff --git a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/ScriptJvmCompilerImpls.kt b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/ScriptJvmCompilerImpls.kt index 0c6756d7961..0f706cbf5a8 100644 --- a/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/ScriptJvmCompilerImpls.kt +++ b/plugins/scripting/scripting-compiler/src/org/jetbrains/kotlin/scripting/compiler/plugin/impl/ScriptJvmCompilerImpls.kt @@ -132,6 +132,23 @@ private fun compileImpl( } } +internal fun registerPackageFragmetProvidersIfNeeded( + scriptCompilationConfiguration: ScriptCompilationConfiguration, + environment: KotlinCoreEnvironment +) { + scriptCompilationConfiguration[ScriptCompilationConfiguration.dependencies]?.forEach { dependency -> + if (dependency is JvmDependencyFromClassLoader) { + // TODO: consider implementing deduplication + PackageFragmentProviderExtension.registerExtension( + environment.project, + PackageFragmentFromClassLoaderProviderExtension( + dependency.classLoaderGetter, scriptCompilationConfiguration, environment.configuration + ) + ) + } + } +} + private fun doCompile( context: SharedScriptCompilationContext, script: SourceCode, @@ -141,14 +158,7 @@ private fun doCompile( getScriptConfiguration: (KtFile) -> ScriptCompilationConfiguration ): ResultWithDiagnostics> { - context.baseScriptCompilationConfiguration[ScriptCompilationConfiguration.dependencies]?.forEach { dependency -> - if (dependency is JvmDependencyFromClassLoader) { - PackageFragmentProviderExtension.registerExtension( - context.environment.project, - PackageFragmentFromClassLoaderProviderExtension(dependency.classLoaderGetter, context.baseScriptCompilationConfiguration) - ) - } - } + registerPackageFragmetProvidersIfNeeded(getScriptConfiguration(sourceFiles.first()), context.environment) val analysisResult = analyze(sourceFiles, context.environment)