diff --git a/.idea/modules.xml b/.idea/modules.xml index 2823659c3a1..9bd6182e30c 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -92,6 +92,7 @@ + diff --git a/build.xml b/build.xml index 7075210a608..fac2b7730b8 100644 --- a/build.xml +++ b/build.xml @@ -766,7 +766,7 @@ - + @@ -786,6 +786,27 @@ + + + + + + + + + + + + + + + + + + + + @@ -878,6 +899,29 @@ + + + + + + + + + + + + + + + + + + + + + + + @@ -1340,11 +1384,11 @@ depends="builtins,stdlib,kotlin-test,core,reflection,pack-runtime,pack-runtime-sources,script-runtime,mock-runtime-for-test"/> processedFiles = Sets.newHashSet(); final List result = Lists.newArrayList(); + final PreprocessedFileCreator virtualFileCreator = new PreprocessedFileCreator(project); + for (String sourceRootPath : sourceRoots) { if (sourceRootPath == null) { continue; @@ -190,7 +193,8 @@ public class CompileEnvironmentUtil { @Override public Unit invoke(File file) { if (file.isFile()) { - VirtualFile virtualFile = localFileSystem.findFileByPath(file.getAbsolutePath()); + VirtualFile originalVirtualFile = localFileSystem.findFileByPath(file.getAbsolutePath()); + VirtualFile virtualFile = originalVirtualFile != null ? virtualFileCreator.create(originalVirtualFile) : null; if (virtualFile != null && !processedFiles.contains(virtualFile)) { processedFiles.add(virtualFile); PsiFile psiFile = PsiManager.getInstance(project).findFile(virtualFile); diff --git a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt index b214330606d..38ad03eac1b 100644 --- a/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt +++ b/compiler/cli/src/org/jetbrains/kotlin/cli/jvm/compiler/KotlinCoreEnvironment.kt @@ -85,6 +85,7 @@ import org.jetbrains.kotlin.codegen.extensions.ExpressionCodegenExtension import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar import org.jetbrains.kotlin.config.* import org.jetbrains.kotlin.extensions.DeclarationAttributeAltererExtension +import org.jetbrains.kotlin.extensions.PreprocessedVirtualFileFactoryExtension import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor import org.jetbrains.kotlin.idea.KotlinFileType import org.jetbrains.kotlin.load.kotlin.KotlinBinaryClassCache @@ -160,6 +161,7 @@ class KotlinCoreEnvironment private constructor( PackageFragmentProviderExtension.registerExtensionPoint(project) StorageComponentContainerContributor.registerExtensionPoint(project) DeclarationAttributeAltererExtension.registerExtensionPoint(project) + PreprocessedVirtualFileFactoryExtension.registerExtensionPoint(project) for (registrar in configuration.getList(ComponentRegistrar.PLUGIN_COMPONENT_REGISTRARS)) { registrar.registerProjectComponents(project, configuration) diff --git a/compiler/frontend/src/org/jetbrains/kotlin/extensions/PreprocessedVirtualFileFactoryExtension.kt b/compiler/frontend/src/org/jetbrains/kotlin/extensions/PreprocessedVirtualFileFactoryExtension.kt new file mode 100644 index 00000000000..091c4cfe1ea --- /dev/null +++ b/compiler/frontend/src/org/jetbrains/kotlin/extensions/PreprocessedVirtualFileFactoryExtension.kt @@ -0,0 +1,50 @@ +/* + * Copyright 2010-2017 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.extensions + +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.testFramework.LightVirtualFile +import org.jetbrains.kotlin.utils.addToStdlib.firstNotNullResult + +/** + * The interface for the extensions that are used to substitute VirtualFile on the creation of KtFile, allows to preprocess a file before + * lexing and parsing + */ +interface PreprocessedVirtualFileFactoryExtension { + companion object : ProjectExtensionDescriptor( + "org.jetbrains.kotlin.preprocessedVirtualFileFactoryExtension", + PreprocessedVirtualFileFactoryExtension::class.java + ) + fun isPassThrough(): Boolean + + fun createPreprocessedFile(file: VirtualFile?): VirtualFile? + fun createPreprocessedLightFile(file: LightVirtualFile?): LightVirtualFile? +} + +class PreprocessedFileCreator(val project: Project) { + + private val validExts: Array by lazy { + PreprocessedVirtualFileFactoryExtension.getInstances(project).filterNot { it.isPassThrough() }.toTypedArray() + } + + fun create(file: VirtualFile): VirtualFile = validExts.firstNotNullResult { it.createPreprocessedFile(file) } ?: file + + // unused now, but could be used in the IDE at some point + fun createLight(file: LightVirtualFile): LightVirtualFile = validExts.firstNotNullResult { it.createPreprocessedLightFile(file) } ?: file +} + diff --git a/core/util.runtime/src/org/jetbrains/kotlin/utils/addToStdlib.kt b/core/util.runtime/src/org/jetbrains/kotlin/utils/addToStdlib.kt index 46095653edd..136e333ccaa 100644 --- a/core/util.runtime/src/org/jetbrains/kotlin/utils/addToStdlib.kt +++ b/core/util.runtime/src/org/jetbrains/kotlin/utils/addToStdlib.kt @@ -118,6 +118,14 @@ inline fun Iterable.firstNotNullResult(transform: (T) -> R?): R? return null } +inline fun Array.firstNotNullResult(transform: (T) -> R?): R? { + for (element in this) { + val result = transform(element) + if (result != null) return result + } + return null +} + inline fun Iterable.sumByLong(selector: (T) -> Long): Long { var sum: Long = 0 for (element in this) { diff --git a/non-compiler-tests/non-compiler-tests.iml b/non-compiler-tests/non-compiler-tests.iml index 3b11639af68..00538a12708 100644 --- a/non-compiler-tests/non-compiler-tests.iml +++ b/non-compiler-tests/non-compiler-tests.iml @@ -34,5 +34,6 @@ + \ No newline at end of file diff --git a/plugins/source-sections/source-sections-compiler/source-sections-compiler.iml b/plugins/source-sections/source-sections-compiler/source-sections-compiler.iml new file mode 100644 index 00000000000..d005e5c5fdb --- /dev/null +++ b/plugins/source-sections/source-sections-compiler/source-sections-compiler.iml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/source-sections/source-sections-compiler/src/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor b/plugins/source-sections/source-sections-compiler/src/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor new file mode 100644 index 00000000000..06b9af7fa14 --- /dev/null +++ b/plugins/source-sections/source-sections-compiler/src/META-INF/services/org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor @@ -0,0 +1 @@ +org.jetbrains.kotlin.sourceSections.SourceSectionsCommandLineProcessor \ No newline at end of file diff --git a/plugins/source-sections/source-sections-compiler/src/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar b/plugins/source-sections/source-sections-compiler/src/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar new file mode 100644 index 00000000000..784b7430fa9 --- /dev/null +++ b/plugins/source-sections/source-sections-compiler/src/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar @@ -0,0 +1 @@ +org.jetbrains.kotlin.sourceSections.SourceSectionsComponentRegistrar \ No newline at end of file diff --git a/plugins/source-sections/source-sections-compiler/src/org/jetbrains/kotlin/sourceSections/FilteredSectionsVirtualFile.kt b/plugins/source-sections/source-sections-compiler/src/org/jetbrains/kotlin/sourceSections/FilteredSectionsVirtualFile.kt new file mode 100644 index 00000000000..4c6b02f089f --- /dev/null +++ b/plugins/source-sections/source-sections-compiler/src/org/jetbrains/kotlin/sourceSections/FilteredSectionsVirtualFile.kt @@ -0,0 +1,145 @@ +/* + * Copyright 2010-2017 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.sourceSections + +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileSystem +import com.intellij.testFramework.LightVirtualFile +import org.jetbrains.kotlin.lexer.KotlinLexer +import org.jetbrains.kotlin.lexer.KtTokens +import java.io.ByteArrayInputStream +import java.io.InputStream +import java.io.OutputStream +import java.nio.ByteBuffer +import java.nio.CharBuffer +import java.nio.charset.Charset + +class FilteredSectionsVirtualFile(val delegate: VirtualFile, val sectionIds: Collection) : VirtualFile() { + + override fun refresh(asynchronous: Boolean, recursive: Boolean, postRunnable: Runnable?) { + delegate.refresh(asynchronous, recursive, postRunnable) + } + + override fun getLength(): Long = delegate.length + override fun getFileSystem(): VirtualFileSystem = delegate.fileSystem + override fun getPath(): String = delegate.path + override fun isDirectory(): Boolean = delegate.isDirectory + override fun getTimeStamp(): Long = delegate.timeStamp + override fun getName(): String = delegate.name + + override fun contentsToByteArray(): ByteArray = filterByteContents(sectionIds, delegate.contentsToByteArray(), delegate.charset) + + override fun getInputStream(): InputStream = ByteArrayInputStream(contentsToByteArray()) + + override fun isValid(): Boolean =delegate.isValid + override fun getParent(): VirtualFile = delegate.parent + override fun getChildren(): Array = delegate.children + override fun isWritable(): Boolean = delegate.isWritable + override fun getOutputStream(requestor: Any?, newModificationStamp: Long, newTimeStamp: Long): OutputStream = + delegate.getOutputStream(requestor, newModificationStamp, newTimeStamp) +} + +class FilteredSectionsLightVirtualFile(val delegate: LightVirtualFile, val sectionIds: Collection) : LightVirtualFile(delegate.name, delegate.fileType, delegate.content, delegate.charset, delegate.modificationStamp) { + override fun getContent(): CharSequence = filterStringBuilderContents(StringBuilder(delegate.content), sectionIds) + override fun contentsToByteArray(): ByteArray = filterByteContents(sectionIds, delegate.contentsToByteArray(), delegate.charset) +} + +private fun filterByteContents(sectionIds: Collection, bytes: ByteArray, charset: Charset): ByteArray { + val content = StringBuilder(charset.decode(ByteBuffer.wrap(bytes))) + filterStringBuilderContents(content, sectionIds) + val buffer = charset.encode(CharBuffer.wrap(content)) + return if (buffer.limit() == buffer.capacity()) buffer.array() + else { + val res = ByteArray(buffer.limit()) + buffer.get(res) + res + } +} + +private fun filterStringBuilderContents(content: StringBuilder, sectionIds: Collection): StringBuilder { + var curPos = 0 + val sectionsIter = FilteredSectionsTokensRangeIterator(content, sectionIds) + for (range in sectionsIter) { + for (i in curPos..range.start - 1) { + if (content[i] != '\n') { + content.setCharAt(i, ' ') + } + } + curPos = range.end + } + for (i in curPos..content.length - 1) { + if (content[i] != '\n') { + content.setCharAt(i, ' ') + } + } + return content +} + + +private class TokenRange(val start: Int, val end: Int) + +private class FilteredSectionsTokensRangeIterator(script: CharSequence, val sectionIds: Collection) : Iterator { + + private val lexer = KotlinLexer().apply { + start(script) + } + + private var currentRange: TokenRange? = advanceToNextFilteredSection() + + fun advanceToNextFilteredSection(): TokenRange? { + + fun KotlinLexer.skipWhiteSpaceAndComments() { + while (tokenType in KtTokens.WHITE_SPACE_OR_COMMENT_BIT_SET) { + advance() + } + } + + var depth = 0 + var sectionStartPos = 0 + var inside = false + with(lexer) { + loop@ while (tokenType != null) { + if (!inside && depth == 0 && tokenType == KtTokens.IDENTIFIER && tokenText in sectionIds) { + sectionStartPos = currentPosition.offset + advance() + skipWhiteSpaceAndComments() + inside = (tokenType == KtTokens.LBRACE) + } + when (tokenType) { + KtTokens.LBRACE -> depth += 1 + KtTokens.RBRACE -> depth -= 1 + } + if (inside && depth == 0) { + advance() + break@loop + } + advance() + } + } + return if (lexer.tokenType == null && !inside) null + else TokenRange(sectionStartPos, lexer.currentPosition.offset) + } + + override fun hasNext(): Boolean = currentRange != null + + override fun next(): TokenRange { + val ret = currentRange + currentRange = advanceToNextFilteredSection() + return ret!! + } +} + diff --git a/plugins/source-sections/source-sections-compiler/src/org/jetbrains/kotlin/sourceSections/FilteredSectionsVirtualFileExtension.kt b/plugins/source-sections/source-sections-compiler/src/org/jetbrains/kotlin/sourceSections/FilteredSectionsVirtualFileExtension.kt new file mode 100644 index 00000000000..347d37ef6ec --- /dev/null +++ b/plugins/source-sections/source-sections-compiler/src/org/jetbrains/kotlin/sourceSections/FilteredSectionsVirtualFileExtension.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2010-2017 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.sourceSections + +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.testFramework.LightVirtualFile +import org.jetbrains.kotlin.extensions.PreprocessedVirtualFileFactoryExtension + +class FilteredSectionsVirtualFileExtension(val allowedSections: Set?) : PreprocessedVirtualFileFactoryExtension { + + override fun isPassThrough(): Boolean = allowedSections == null || allowedSections.isEmpty() + + override fun createPreprocessedFile(file: VirtualFile?): VirtualFile? = + when { + file == null || allowedSections == null || allowedSections.isEmpty() -> file + file is LightVirtualFile -> FilteredSectionsLightVirtualFile(file, allowedSections) + else -> FilteredSectionsVirtualFile(file, allowedSections) + } + + override fun createPreprocessedLightFile(file: LightVirtualFile?): LightVirtualFile? = + when { + file == null || allowedSections == null || allowedSections.isEmpty() -> file + else -> FilteredSectionsLightVirtualFile(file, allowedSections) + } +} \ No newline at end of file diff --git a/plugins/source-sections/source-sections-compiler/src/org/jetbrains/kotlin/sourceSections/SourceSectionsPlugin.kt b/plugins/source-sections/source-sections-compiler/src/org/jetbrains/kotlin/sourceSections/SourceSectionsPlugin.kt new file mode 100644 index 00000000000..800bdf497be --- /dev/null +++ b/plugins/source-sections/source-sections-compiler/src/org/jetbrains/kotlin/sourceSections/SourceSectionsPlugin.kt @@ -0,0 +1,68 @@ +/* + * Copyright 2010-2017 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.sourceSections + +import com.intellij.mock.MockProject +import com.intellij.openapi.project.Project +import org.jetbrains.kotlin.compiler.plugin.CliOption +import org.jetbrains.kotlin.compiler.plugin.CliOptionProcessingException +import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor +import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +import org.jetbrains.kotlin.config.CompilerConfiguration +import org.jetbrains.kotlin.config.CompilerConfigurationKey +import org.jetbrains.kotlin.extensions.PreprocessedVirtualFileFactoryExtension +import org.jetbrains.kotlin.extensions.PreprocessedVirtualFileFactoryExtension.Companion + +object SourceSectionsConfigurationKeys { + val SECTIONS_OPTION: CompilerConfigurationKey> = + CompilerConfigurationKey.create("allowed section name") +} + +class SourceSectionsCommandLineProcessor : CommandLineProcessor { + companion object { + val SECTIONS_OPTION = CliOption("allowedSection", "", "Allowed section name", + required = true, allowMultipleOccurrences = true) + + val PLUGIN_ID = "org.jetbrains.kotlin.sourceSections" + } + + override val pluginId = PLUGIN_ID + override val pluginOptions = listOf(SECTIONS_OPTION) + + override fun processOption(option: CliOption, value: String, configuration: CompilerConfiguration) = when (option) { + SECTIONS_OPTION -> { + val paths = configuration.getList(SourceSectionsConfigurationKeys.SECTIONS_OPTION).toMutableList() + paths.add(value) + configuration.put(SourceSectionsConfigurationKeys.SECTIONS_OPTION, paths) + } + else -> throw CliOptionProcessingException("Unknown option: ${option.name}") + } +} + +class SourceSectionsComponentRegistrar : ComponentRegistrar { + override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { + val sections = configuration.get(SourceSectionsConfigurationKeys.SECTIONS_OPTION) ?: return + registerAllowedSourceSections(project, sections) + } +} + +fun registerAllowedSourceSections(project: Project, sections: List) { + if (!sections.isEmpty()) { + PreprocessedVirtualFileFactoryExtension.registerExtension(project, FilteredSectionsVirtualFileExtension(sections.toHashSet())) + } +} + diff --git a/plugins/source-sections/source-sections-compiler/testData/sourceSectionsBasics.kts b/plugins/source-sections/source-sections-compiler/testData/sourceSectionsBasics.kts new file mode 100644 index 00000000000..d2559359b57 --- /dev/null +++ b/plugins/source-sections/source-sections-compiler/testData/sourceSectionsBasics.kts @@ -0,0 +1,22 @@ + +println("should not be printed") + +fun unlistedSectionName(body: () -> Unit): Unit = body() + +let { + println("Hello, World!") +} + +println("ignore here") + +unlistedSectionName { + println("should not be printed too") +} + +println("ignore here") + +apply { + println("That's all, folks!") +} + +println("ignore here") diff --git a/plugins/source-sections/source-sections-compiler/testData/sourceSectionsBasics.kts.filtered b/plugins/source-sections/source-sections-compiler/testData/sourceSectionsBasics.kts.filtered new file mode 100644 index 00000000000..cc4c93da87d --- /dev/null +++ b/plugins/source-sections/source-sections-compiler/testData/sourceSectionsBasics.kts.filtered @@ -0,0 +1,22 @@ + + + + + +let { + println("Hello, World!") +} + + + + + + + + + +apply { + println("That's all, folks!") +} + + diff --git a/plugins/source-sections/source-sections-compiler/testData/sourceSectionsBasics.kts.out b/plugins/source-sections/source-sections-compiler/testData/sourceSectionsBasics.kts.out new file mode 100644 index 00000000000..8f9188b07ba --- /dev/null +++ b/plugins/source-sections/source-sections-compiler/testData/sourceSectionsBasics.kts.out @@ -0,0 +1,2 @@ +Hello, World! +That's all, folks! diff --git a/plugins/source-sections/source-sections-compiler/tests/org/jetbrains/kotlin/sourceSections/SourceSectionsTest.kt b/plugins/source-sections/source-sections-compiler/tests/org/jetbrains/kotlin/sourceSections/SourceSectionsTest.kt new file mode 100644 index 00000000000..d515488e708 --- /dev/null +++ b/plugins/source-sections/source-sections-compiler/tests/org/jetbrains/kotlin/sourceSections/SourceSectionsTest.kt @@ -0,0 +1,270 @@ +/* + * Copyright 2010-2017 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.sourceSections + +import com.intellij.openapi.vfs.StandardFileSystems +import junit.framework.TestCase +import org.jetbrains.kotlin.cli.AbstractCliTest +import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys +import org.jetbrains.kotlin.cli.common.messages.* +import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler +import org.jetbrains.kotlin.cli.jvm.compiler.EnvironmentConfigFiles +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment +import org.jetbrains.kotlin.cli.jvm.compiler.KotlinToJVMBytecodeCompiler +import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar +import org.jetbrains.kotlin.config.JVMConfigurationKeys +import org.jetbrains.kotlin.config.addKotlinSourceRoots +import org.jetbrains.kotlin.daemon.client.DaemonReportingTargets +import org.jetbrains.kotlin.daemon.client.KotlinCompilerClient +import org.jetbrains.kotlin.daemon.common.* +import org.jetbrains.kotlin.script.StandardScriptDefinition +import org.jetbrains.kotlin.test.ConfigurationKind +import org.jetbrains.kotlin.test.KotlinTestUtils +import org.jetbrains.kotlin.test.TestCaseWithTmpdir +import org.jetbrains.kotlin.test.TestJdkKind +import org.jetbrains.kotlin.utils.KotlinPaths +import org.jetbrains.kotlin.utils.PathUtil +import org.jetbrains.kotlin.utils.tryConstructClassFromStringArgs +import java.io.* +import java.lang.management.ManagementFactory +import java.net.URLClassLoader +import java.nio.charset.Charset +import java.util.concurrent.TimeUnit + +class SourceSectionsTest : TestCaseWithTmpdir() { + + companion object { + val TEST_ALLOWED_SECTIONS = listOf("let", "apply") // using standard function names that can be used as sections in the script context without crafting special ones + val TEST_DATA_DIR = File(KotlinTestUtils.getHomeDirectory(), "plugins/source-sections/source-sections-compiler/testData") + } + + private val kotlinPaths: KotlinPaths by lazy { + val paths = PathUtil.getKotlinPathsForDistDirectory() + TestCase.assertTrue("Lib directory doesn't exist. Run 'ant dist'", paths.libPath.absoluteFile.isDirectory) + paths + } + + val compilerClassPath = listOf(kotlinPaths.compilerPath) + val scriptRuntimeClassPath = listOf( kotlinPaths.runtimePath, kotlinPaths.scriptRuntimePath) + val sourceSectionsPluginJar = File(kotlinPaths.libPath, "source-sections-compiler-plugin.jar") + val compilerId by lazy(LazyThreadSafetyMode.NONE) { CompilerId.makeCompilerId(compilerClassPath) } + + private fun createEnvironment(vararg sources: String, withSourceSectionsPlugin: Boolean = true): KotlinCoreEnvironment { + val configuration = KotlinTestUtils.newConfiguration(ConfigurationKind.NO_KOTLIN_REFLECT, TestJdkKind.FULL_JDK) + + configuration.addKotlinSourceRoots(sources.asList()) + configuration.put( + CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, + PrintingMessageCollector(System.err, MessageRenderer.PLAIN_FULL_PATHS, false) + ) + configuration.add(JVMConfigurationKeys.SCRIPT_DEFINITIONS, StandardScriptDefinition) + if (withSourceSectionsPlugin) { + configuration.addAll(SourceSectionsConfigurationKeys.SECTIONS_OPTION, TEST_ALLOWED_SECTIONS) + configuration.add(ComponentRegistrar.PLUGIN_COMPONENT_REGISTRARS, SourceSectionsComponentRegistrar()) + } + + val environment = KotlinCoreEnvironment.createForTests(testRootDisposable, configuration, EnvironmentConfigFiles.JVM_CONFIG_FILES) + return environment + } + + private data class SourceToExpectedResults(val source: File, val expectedResults: File) + + private fun getTestFiles(expectedExt: String): List { + val testDataFiles = TEST_DATA_DIR.listFiles() + val sourceToExpected = testDataFiles.filter { it.isFile && it.extension == "kts" } + .mapNotNull { testFile -> + testDataFiles.find { it.isFile && it.name == testFile.name + expectedExt }?.let { SourceToExpectedResults(testFile, it) } + } + TestCase.assertTrue("No test files found", sourceToExpected.isNotEmpty()) + return sourceToExpected + } + + private fun InputStream.trimmedLines(charset: Charset): List = use { + bufferedReader(charset) + .lineSequence() + .map(String::trimEnd) + .toList() + } + + fun testSourceSectionsFilter() { + val sourceToFiltered = getTestFiles(".filtered") + + createEnvironment() // creates VirtualFileManager + val fileCreator = FilteredSectionsVirtualFileExtension(TEST_ALLOWED_SECTIONS.toSet()) + + sourceToFiltered.forEach { (source, expectedResult) -> + val filteredVF = fileCreator.createPreprocessedFile(StandardFileSystems.local().findFileByPath(source.canonicalPath)) + TestCase.assertNotNull("Cannot generate preprocessed file", filteredVF) + val expected = expectedResult.inputStream().trimmedLines(Charset.defaultCharset()) + val actual = ByteArrayInputStream(filteredVF!!.contentsToByteArray()).trimmedLines(filteredVF.charset) + TestCase.assertEquals("Unexpected result on preprocessing file '${source.name}'", expected, actual) + } + } + + fun testSourceSectionsRun() { + val sourceToOutput = getTestFiles(".out") + + sourceToOutput.forEach { (source, expectedOutput) -> + val environment = createEnvironment(source.canonicalPath) + val scriptClass = KotlinToJVMBytecodeCompiler.compileScript(environment, Thread.currentThread().contextClassLoader) + TestCase.assertNotNull("Compilation errors", scriptClass) + verifyScriptOutput(scriptClass, expectedOutput) + } + } + + fun testSourceSectionsRunBench() { + val mxBeans = ManagementFactory.getThreadMXBean() + val (source, _) = getTestFiles(".out").first() + + // warming up application environment + KotlinToJVMBytecodeCompiler.compileScript(createEnvironment(source.canonicalPath, withSourceSectionsPlugin = false), Thread.currentThread().contextClassLoader) + + val times = generateSequence { + val t0 = mxBeans.threadCpuTime() + KotlinToJVMBytecodeCompiler.compileScript(createEnvironment(source.canonicalPath, withSourceSectionsPlugin = false), Thread.currentThread().contextClassLoader) + val t1 = mxBeans.threadCpuTime() + KotlinToJVMBytecodeCompiler.compileScript(createEnvironment(source.canonicalPath, withSourceSectionsPlugin = true), Thread.currentThread().contextClassLoader) + val t2 = mxBeans.threadCpuTime() + Triple(t1 - t0, t2 - t1, t2 - t1) + }.take(10).toList() + + val adjustedMaxDiff = times.sortedByDescending { (_, _, diff) -> diff }.drop(2).first() + + fun Long.ms() = TimeUnit.NANOSECONDS.toMillis(this) + TestCase.assertTrue("sourceSections plugin brings too much overheads: ${times.joinToString { "(${it.first.ms()}, ${it.second.ms()})" }} (expecting it to be faster than regular compilation due to less lines compiled)", + adjustedMaxDiff.third < 20 /* assuming it measurement error */ || adjustedMaxDiff.first >= adjustedMaxDiff.second ) + } + + fun testSourceSectionCompileLocal() { + val sourceToOutput = getTestFiles(".out") + + sourceToOutput.forEach { (source, expectedOutput) -> + + val args = listOf(source.canonicalPath, "-d", tmpdir.canonicalPath, + "-Xplugin", sourceSectionsPluginJar.canonicalPath, + "-P", TEST_ALLOWED_SECTIONS.joinToString(",") { "plugin:${SourceSectionsCommandLineProcessor.PLUGIN_ID}:${SourceSectionsCommandLineProcessor.SECTIONS_OPTION.name}=$it" }) + val (output, code) = AbstractCliTest.executeCompilerGrabOutput(K2JVMCompiler(), args) + + TestCase.assertEquals("Compilation failed:\n$output", 0, code.code) + + val scriptClass = loadScriptClass(File(tmpdir, source.nameWithoutExtension.capitalize() + ".class")) + + verifyScriptOutput(scriptClass, expectedOutput) + } + } + + fun testSourceSectionCompileOnDaemon() { + val sourceToOutput = getTestFiles(".out") + + withFlagFile("sourceSections", ".alive") { aliveFile -> + + val daemonOptions = DaemonOptions(runFilesPath = File(tmpdir, getTestName(true)).absolutePath, verbose = true, reportPerf = true) + val daemonJVMOptions = configureDaemonJVMOptions(inheritMemoryLimits = false, inheritAdditionalProperties = false) + val messageCollector = TestMessageCollector() + + val daemonWithSession = KotlinCompilerClient.connectAndLease(compilerId, aliveFile, daemonJVMOptions, daemonOptions, + DaemonReportingTargets(messageCollector = messageCollector), autostart = true, leaseSession = true) + assertNotNull("failed to connect daemon", daemonWithSession) + + try { + + sourceToOutput.forEach { (source, expectedOutput) -> + + val args = arrayOf(source.canonicalPath, "-d", tmpdir.canonicalPath, + "-Xplugin", sourceSectionsPluginJar.canonicalPath, + "-P", TEST_ALLOWED_SECTIONS.joinToString(",") { "plugin:${SourceSectionsCommandLineProcessor.PLUGIN_ID}:${SourceSectionsCommandLineProcessor.SECTIONS_OPTION.name}=$it" }) + + messageCollector.clear() + val outputs = arrayListOf() + + val code = KotlinCompilerClient.compile(daemonWithSession!!.first, daemonWithSession.second, CompileService.TargetPlatform.JVM, + args, messageCollector, + { outFile, srcFiles -> outputs.add(OutputMessageUtil.Output(srcFiles, outFile)) }, + reportSeverity = ReportSeverity.DEBUG) + + TestCase.assertEquals("Compilation failed:\n${messageCollector.messages.joinToString("\n")}", 0, code) + TestCase.assertFalse("Compilation failed:\n${messageCollector.messages.joinToString("\n")}", messageCollector.hasErrors()) + val scriptClassFile = outputs.first().outputFile + TestCase.assertEquals("unexpected class file generated", source.nameWithoutExtension.capitalize(), scriptClassFile?.nameWithoutExtension) + + verifyScriptOutput(loadScriptClass(scriptClassFile), expectedOutput) + } + } + finally { + daemonWithSession!!.first.shutdown() + } + } + } + + private fun loadScriptClass(scriptClassFile: File?): Class<*>? { + val cl = URLClassLoader((scriptRuntimeClassPath + tmpdir).map { it.toURI().toURL() }.toTypedArray()) + val scriptClass = cl.loadClass(scriptClassFile!!.nameWithoutExtension) + + TestCase.assertNotNull("Unable to load class $scriptClassFile", scriptClass) + return scriptClass + } + + private fun verifyScriptOutput(scriptClass: Class<*>?, expectedOutput: File) { + val scriptOut = captureOut { + tryConstructClassFromStringArgs(scriptClass!!, emptyList()) + } + val expected = expectedOutput.readText() + + TestCase.assertEquals(expected, scriptOut) + } +} + +internal inline fun withFlagFile(prefix: String, suffix: String? = null, body: (File) -> Unit) { + val file = createTempFile(prefix, suffix) + try { + body(file) + } + finally { + file.delete() + } +} + +internal fun captureOut(body: () -> Unit): String { + val outStream = ByteArrayOutputStream() + val prevOut = System.out + System.setOut(PrintStream(outStream)) + try { + body() + } + finally { + System.out.flush() + System.setOut(prevOut) + } + return outStream.toString() +} + +class TestMessageCollector : MessageCollector { + + data class Message(val severity: CompilerMessageSeverity, val message: String, val location: CompilerMessageLocation) + + val messages = arrayListOf() + + override fun clear() { + messages.clear() + } + + override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageLocation) { + messages.add(Message(severity, message, location)) + } + + override fun hasErrors(): Boolean = messages.any { it.severity == CompilerMessageSeverity.EXCEPTION || it.severity == CompilerMessageSeverity.ERROR } +}