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 }
+}