From 39f7ecc9a3b885c200e2e65f8de687d09fa78b87 Mon Sep 17 00:00:00 2001 From: Alexey Tsvetkov Date: Tue, 13 Mar 2018 21:18:07 +0300 Subject: [PATCH] 181: Reimplement constant search in JPS The API in Intellij have been changed after the PR was merged (see https://github.com/JetBrains/intellij-community/commit/8227d8e2dd4d98d2ff248a1b193ba31831ddef50) This commit implements new API. Also mocked Kotlin constant search is removed from JPS tests. Mocked Java search is in place, but now it is does not use hardcoded file and constant names. #KT-16091 fixed --- .../build/AbstractIncrementalJpsTest.kt.181 | 538 ++++++++++++++++++ .../IncrementalConstantSearchTest.kt.181 | 43 ++ ...jps.builders.java.JavaBuilderExtension.181 | 1 + .../build/KotlinJavaBuilderExtension.kt.181 | 65 +++ 4 files changed, 647 insertions(+) create mode 100644 jps-plugin/jps-tests/test/org/jetbrains/kotlin/jps/build/AbstractIncrementalJpsTest.kt.181 create mode 100644 jps-plugin/jps-tests/test/org/jetbrains/kotlin/jps/build/IncrementalConstantSearchTest.kt.181 create mode 100644 jps-plugin/src/META-INF/services/org.jetbrains.jps.builders.java.JavaBuilderExtension.181 create mode 100644 jps-plugin/src/org/jetbrains/kotlin/jps/build/KotlinJavaBuilderExtension.kt.181 diff --git a/jps-plugin/jps-tests/test/org/jetbrains/kotlin/jps/build/AbstractIncrementalJpsTest.kt.181 b/jps-plugin/jps-tests/test/org/jetbrains/kotlin/jps/build/AbstractIncrementalJpsTest.kt.181 new file mode 100644 index 00000000000..9af8ba59fd8 --- /dev/null +++ b/jps-plugin/jps-tests/test/org/jetbrains/kotlin/jps/build/AbstractIncrementalJpsTest.kt.181 @@ -0,0 +1,538 @@ +/* + * Copyright 2010-2016 JetBrains s.r.o. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.jetbrains.kotlin.jps.build + +import com.intellij.openapi.Disposable +import com.intellij.openapi.util.Disposer +import com.intellij.openapi.util.io.FileUtil +import com.intellij.openapi.util.io.FileUtilRt +import com.intellij.testFramework.TestLoggerFactory +import com.intellij.testFramework.UsefulTestCase +import com.intellij.util.concurrency.FixedFuture +import junit.framework.TestCase +import org.apache.log4j.ConsoleAppender +import org.apache.log4j.Level +import org.apache.log4j.Logger +import org.apache.log4j.PatternLayout +import org.jetbrains.jps.ModuleChunk +import org.jetbrains.jps.api.CanceledStatus +import org.jetbrains.jps.builders.BuildResult +import org.jetbrains.jps.builders.CompileScopeTestBuilder +import org.jetbrains.jps.builders.impl.BuildDataPathsImpl +import org.jetbrains.jps.builders.impl.logging.ProjectBuilderLoggerBase +import org.jetbrains.jps.builders.java.dependencyView.Callbacks +import org.jetbrains.jps.builders.logging.BuildLoggingManager +import org.jetbrains.jps.cmdline.ProjectDescriptor +import org.jetbrains.jps.incremental.* +import org.jetbrains.jps.incremental.messages.BuildMessage +import org.jetbrains.jps.model.JpsModuleRootModificationUtil +import org.jetbrains.jps.model.java.JpsJavaDependencyScope +import org.jetbrains.jps.model.java.JpsJavaExtensionService +import org.jetbrains.jps.util.JpsPathUtil +import org.jetbrains.kotlin.config.IncrementalCompilation +import org.jetbrains.kotlin.incremental.CacheVersion +import org.jetbrains.kotlin.incremental.LookupSymbol +import org.jetbrains.kotlin.incremental.isJavaFile +import org.jetbrains.kotlin.incremental.testingUtils.* +import org.jetbrains.kotlin.jps.incremental.getKotlinCache +import org.jetbrains.kotlin.jps.incremental.withLookupStorage +import org.jetbrains.kotlin.test.KotlinTestUtils +import org.jetbrains.kotlin.utils.Printer +import org.jetbrains.kotlin.utils.keysToMap +import java.io.* +import java.util.* +import java.util.concurrent.Future +import kotlin.reflect.jvm.javaField + +abstract class AbstractIncrementalJpsTest( + private val allowNoFilesWithSuffixInTestData: Boolean = false, + private val checkDumpsCaseInsensitively: Boolean = false, + private val allowNoBuildLogFileInTestData: Boolean = false +) : BaseKotlinJpsBuildTestCase() { + companion object { + private val COMPILATION_FAILED = "COMPILATION FAILED" + + // change to "/tmp" or anything when default is too long (for easier debugging) + private val TEMP_DIRECTORY_TO_USE = File(FileUtilRt.getTempDirectory()) + + private val DEBUG_LOGGING_ENABLED = System.getProperty("debug.logging.enabled") == "true" + } + + protected lateinit var testDataDir: File + protected lateinit var workDir: File + protected lateinit var projectDescriptor: ProjectDescriptor + // is used to compare lookup dumps in a human readable way (lookup symbols are hashed in an actual lookup storage) + protected lateinit var lookupsDuringTest: MutableSet + private var isICEnabledBackup: Boolean = false + + protected var mapWorkingToOriginalFile: MutableMap = hashMapOf() + + protected open val buildLogFinder: BuildLogFinder + get() = BuildLogFinder() + + private fun enableDebugLogging() { + com.intellij.openapi.diagnostic.Logger.setFactory(TestLoggerFactory::class.java) + TestLoggerFactory.dumpLogToStdout("") + TestLoggerFactory.enableDebugLogging(testRootDisposable, "#org") + + val console = ConsoleAppender() + console.layout = PatternLayout("%d [%p|%c|%C{1}] %m%n") + console.threshold = Level.ALL + console.activateOptions() + Logger.getRootLogger().addAppender(console) + } + + private var systemPropertiesBackup = run { + val props = System.getProperties() + val output = ByteArrayOutputStream() + props.store(output, "System properties backup") + output.toByteArray() + } + + private fun restoreSystemProperties() { + val input = ByteArrayInputStream(systemPropertiesBackup) + val props = Properties() + props.load(input) + System.setProperties(props) + } + + override fun setUp() { + super.setUp() + lookupsDuringTest = hashSetOf() + isICEnabledBackup = IncrementalCompilation.isEnabled() + IncrementalCompilation.setIsEnabled(true) + + if (DEBUG_LOGGING_ENABLED) { + enableDebugLogging() + } + } + + override fun tearDown() { + restoreSystemProperties() + (AbstractIncrementalJpsTest::myProject).javaField!![this] = null + (AbstractIncrementalJpsTest::projectDescriptor).javaField!![this] = null + (AbstractIncrementalJpsTest::systemPropertiesBackup).javaField!![this] = null + lookupsDuringTest.clear() + IncrementalCompilation.setIsEnabled(isICEnabledBackup) + super.tearDown() + } + + // JPS forces rebuild of all files when JVM constant has been changed and Callbacks.ConstantAffectionResolver + // is not provided, so ConstantAffectionResolver is mocked with empty implementation + // Usages in Kotlin files are expected to be found by KotlinLookupConstantSearch + private val mockConstantSearch: Callbacks.ConstantAffectionResolver? + get() = MockJavaConstantSearch(workDir) + + private fun build(scope: CompileScopeTestBuilder = CompileScopeTestBuilder.make().allModules()): MakeResult { + val workDirPath = FileUtil.toSystemIndependentName(workDir.absolutePath) + + val logger = MyLogger(workDirPath) + projectDescriptor = createProjectDescriptor(BuildLoggingManager(logger)) + + val lookupTracker = TestLookupTracker() + projectDescriptor.project.setTestingContext(TestingContext(lookupTracker, logger)) + + try { + val builder = IncProjectBuilder(projectDescriptor, BuilderRegistry.getInstance(), myBuildParams, CanceledStatus.NULL, mockConstantSearch, true) + val buildResult = BuildResult() + builder.addMessageHandler(buildResult) + builder.build(scope.build(), false) + + lookupTracker.lookups.mapTo(lookupsDuringTest) { LookupSymbol(it.name, it.scopeFqName) } + + if (!buildResult.isSuccessful) { + val errorMessages = + buildResult + .getMessages(BuildMessage.Kind.ERROR) + .map { it.messageText } + .map { it.replace("^.+:\\d+:\\s+".toRegex(), "").trim() } + .joinToString("\n") + return MakeResult(logger.log + "$COMPILATION_FAILED\n" + errorMessages + "\n", true, null) + } + else { + return MakeResult(logger.log, false, createMappingsDump(projectDescriptor)) + } + } + finally { + projectDescriptor.dataManager.flush(false) + projectDescriptor.release() + } + } + + private fun initialMake(): MakeResult { + val makeResult = build() + + val initBuildLogFile = File(testDataDir, "init-build.log") + if (initBuildLogFile.exists()) { + UsefulTestCase.assertSameLinesWithFile(initBuildLogFile.absolutePath, makeResult.log) + } + else { + assertFalse("Initial make failed:\n$makeResult", makeResult.makeFailed) + } + + return makeResult + } + + private fun make(): MakeResult { + return build() + } + + private fun rebuild(): MakeResult { + return build(CompileScopeTestBuilder.rebuild().allModules()) + } + + private fun rebuildAndCheckOutput(makeOverallResult: MakeResult) { + val outDir = File(getAbsolutePath("out")) + val outAfterMake = File(getAbsolutePath("out-after-make")) + + if (outDir.exists()) { + FileUtil.copyDir(outDir, outAfterMake) + } + + val rebuildResult = rebuild() + assertEquals("Rebuild failed: ${rebuildResult.makeFailed}, last make failed: ${makeOverallResult.makeFailed}. Rebuild result: $rebuildResult", + rebuildResult.makeFailed, makeOverallResult.makeFailed) + + if (!outAfterMake.exists()) { + assertFalse(outDir.exists()) + } + else { + assertEqualDirectories(outDir, outAfterMake, makeOverallResult.makeFailed) + } + + if (!makeOverallResult.makeFailed) { + if (checkDumpsCaseInsensitively && rebuildResult.mappingsDump?.toLowerCase() == makeOverallResult.mappingsDump?.toLowerCase()) { + // do nothing + } + else { + TestCase.assertEquals(rebuildResult.mappingsDump, makeOverallResult.mappingsDump) + } + } + + FileUtil.delete(outAfterMake) + } + + private fun clearCachesRebuildAndCheckOutput(makeOverallResult: MakeResult) { + FileUtil.delete(BuildDataPathsImpl(myDataStorageRoot).dataStorageRoot!!) + + rebuildAndCheckOutput(makeOverallResult) + } + + private fun readModuleDependencies(): Map>? { + val dependenciesTxt = File(testDataDir, "dependencies.txt") + if (!dependenciesTxt.exists()) return null + + val result = HashMap>() + for (line in dependenciesTxt.readLines()) { + val split = line.split("->") + val module = split[0] + val dependencies = if (split.size > 1) split[1] else "" + val dependencyList = dependencies.split(",").filterNot { it.isEmpty() } + result[module] = dependencyList.map(::parseDependency) + } + + return result + } + + protected open fun createBuildLog(incrementalMakeResults: List): String = + buildString { + incrementalMakeResults.forEachIndexed { i, makeResult -> + if (i > 0) append("\n") + append("================ Step #${i + 1} =================\n\n") + append(makeResult.log) + } + } + + protected open fun doTest(testDataPath: String) { + testDataDir = File(testDataPath) + workDir = FileUtilRt.createTempDirectory(TEMP_DIRECTORY_TO_USE, "jps-build", null) + Disposer.register(testRootDisposable, Disposable { FileUtilRt.delete(workDir) }) + + val moduleNames = configureModules() + initialMake() + + val otherMakeResults = performModificationsAndMake(moduleNames) + val buildLogFile = buildLogFinder.findBuildLog(testDataDir) + val logs = createBuildLog(otherMakeResults) + + if (buildLogFile != null && buildLogFile.exists()) { + UsefulTestCase.assertSameLinesWithFile(buildLogFile.absolutePath, logs) + } + else if (!allowNoBuildLogFileInTestData) { + throw IllegalStateException("No build log file in $testDataDir") + } + + val lastMakeResult = otherMakeResults.last() + rebuildAndCheckOutput(lastMakeResult) + clearCachesRebuildAndCheckOutput(lastMakeResult) + } + + private fun createMappingsDump(project: ProjectDescriptor) = + createKotlinIncrementalCacheDump(project) + "\n\n\n" + + createLookupCacheDump(project) + "\n\n\n" + + createCommonMappingsDump(project) + "\n\n\n" + + createJavaMappingsDump(project) + + private fun createKotlinIncrementalCacheDump(project: ProjectDescriptor): String { + return buildString { + for (target in project.allModuleTargets.sortedBy { it.presentableName }) { + append("\n") + append(project.dataManager.getKotlinCache(target).dump()) + append("\n\n\n") + } + } + } + + private fun createLookupCacheDump(project: ProjectDescriptor): String { + val sb = StringBuilder() + val p = Printer(sb) + p.println("Begin of Lookup Maps") + p.println() + + project.dataManager.withLookupStorage { lookupStorage -> + lookupStorage.forceGC() + p.print(lookupStorage.dump(lookupsDuringTest)) + } + + p.println() + p.println("End of Lookup Maps") + return sb.toString() + } + + private fun createCommonMappingsDump(project: ProjectDescriptor): String { + val resultBuf = StringBuilder() + val result = Printer(resultBuf) + + result.println("Begin of SourceToOutputMap") + result.pushIndent() + + for (target in project.allModuleTargets) { + result.println(target) + result.pushIndent() + + val mapping = project.dataManager.getSourceToOutputMap(target) + mapping.sources.sorted().forEach { + val outputs = mapping.getOutputs(it)!!.sorted() + if (outputs.isNotEmpty()) { + result.println("source $it -> $outputs") + } + } + + result.popIndent() + } + + result.popIndent() + result.println("End of SourceToOutputMap") + + return resultBuf.toString() + } + + private fun createJavaMappingsDump(project: ProjectDescriptor): String { + val byteArrayOutputStream = ByteArrayOutputStream() + PrintStream(byteArrayOutputStream).use { + project.dataManager.mappings.toStream(it) + } + return byteArrayOutputStream.toString() + } + + protected data class MakeResult(val log: String, val makeFailed: Boolean, val mappingsDump: String?) + + private fun performModificationsAndMake(moduleNames: Set?): List { + val results = arrayListOf() + val modifications = getModificationsToPerform(testDataDir, moduleNames, allowNoFilesWithSuffixInTestData, TouchPolicy.TIMESTAMP) + + for (step in modifications) { + step.forEach { it.perform(workDir, mapWorkingToOriginalFile) } + performAdditionalModifications(step) + if (moduleNames == null) { + preProcessSources(File(workDir, "src")) + } + else { + moduleNames.forEach { preProcessSources(File(workDir, "$it/src")) } + } + + results.add(make()) + } + return results + } + + protected open fun performAdditionalModifications(modifications: List) { + } + + // null means one module + private fun configureModules(): Set? { + fun prepareModuleSources(moduleName: String?) { + val sourceDirName = moduleName?.let { "$it/src" } ?: "src" + val filePrefix = moduleName?.let { "${it}_" } ?: "" + val sourceDestinationDir = File(workDir, sourceDirName) + val sourcesMapping = copyTestSources(testDataDir, sourceDestinationDir, filePrefix) + mapWorkingToOriginalFile.putAll(sourcesMapping) + preProcessSources(sourceDestinationDir) + } + + JpsJavaExtensionService.getInstance().getOrCreateProjectExtension(myProject).outputUrl = JpsPathUtil.pathToUrl(getAbsolutePath("out")) + + val jdk = addJdk("my jdk") + val moduleDependencies = readModuleDependencies() + mapWorkingToOriginalFile = hashMapOf() + + val moduleNames: Set? + if (moduleDependencies == null) { + addModule("module", arrayOf(getAbsolutePath("src")), null, null, jdk) + prepareModuleSources(moduleName = null) + moduleNames = null + } + else { + val nameToModule = moduleDependencies.keys + .keysToMap { addModule(it, arrayOf(getAbsolutePath("$it/src")), null, null, jdk)!! } + + for ((moduleName, dependencies) in moduleDependencies) { + val module = nameToModule[moduleName]!! + + for (dependency in dependencies) { + JpsModuleRootModificationUtil.addDependency(module, nameToModule[dependency.name], + JpsJavaDependencyScope.COMPILE, dependency.exported) + } + } + + for (module in nameToModule.values) { + prepareModuleSources(module.name) + } + + moduleNames = nameToModule.keys + } + AbstractKotlinJpsBuildTestCase.addKotlinStdlibDependency(myProject) + AbstractKotlinJpsBuildTestCase.addKotlinTestDependency(myProject) + return moduleNames + } + + + protected open fun preProcessSources(srcDir: File) { + } + + override fun doGetProjectDir(): File? = workDir + + private class MyLogger(val rootPath: String) : ProjectBuilderLoggerBase(), BuildLogger { + private val markedDirtyBeforeRound = ArrayList() + private val markedDirtyAfterRound = ArrayList() + + override fun actionsOnCacheVersionChanged(actions: List) { + if (actions.size > 1 && actions.any { it != CacheVersion.Action.DO_NOTHING }) { + logLine("Actions after cache changed: $actions") + } + } + + override fun markedAsDirtyBeforeRound(files: Iterable) { + markedDirtyBeforeRound.addAll(files) + } + + override fun markedAsDirtyAfterRound(files: Iterable) { + markedDirtyAfterRound.addAll(files) + } + + override fun buildStarted(context: CompileContext, chunk: ModuleChunk) { + if (!chunk.isDummy(context) && context.projectDescriptor.project.modules.size > 1) { + logLine("Building ${chunk.modules.sortedBy { it.name }.joinToString { it.name }}") + } + } + + override fun afterBuildStarted(context: CompileContext, chunk: ModuleChunk) { + logDirtyFiles(markedDirtyBeforeRound) + } + + override fun buildFinished(exitCode: ModuleLevelBuilder.ExitCode) { + logDirtyFiles(markedDirtyAfterRound) + logLine("Exit code: $exitCode") + logLine("------------------------------------------") + } + + private fun logDirtyFiles(files: MutableList) { + if (files.isEmpty()) return + + logLine("Marked as dirty by Kotlin:") + files.apply { + map { FileUtil.toSystemIndependentName(it.path) } + .sorted() + .forEach { logLine(it) } + + clear() + } + } + + private val logBuf = StringBuilder() + val log: String + get() = logBuf.toString() + + val compiledFiles = hashSetOf() + + override fun isEnabled(): Boolean = true + + override fun logCompiledFiles(files: MutableCollection?, builderName: String?, description: String?) { + super.logCompiledFiles(files, builderName, description) + + if (builderName == KotlinBuilder.KOTLIN_BUILDER_NAME) { + compiledFiles.addAll(files!!) + } + } + + override fun logLine(message: String?) { + logBuf.append(KotlinTestUtils.replaceHashWithStar(message!!.replace("^$rootPath/".toRegex(), " "))).append('\n') + } + } +} + +/** + * Mocks Intellij Java constant search. + * When JPS is run from Intellij, it sends find usages request to IDE (it only searches for references inside Java files). + * + * We rely on heuristics instead of precise usages search. + * A Java file is considered affected if: + * 1. It contains changed field name as a content substring. + * 2. Its simple file name is not equal to a field's owner class simple name (to avoid recompiling field's declaration again) + */ +private class MockJavaConstantSearch(private val workDir: File) : Callbacks.ConstantAffectionResolver { + override fun request( + ownerClassName: String, + fieldName: String, + accessFlags: Int, + fieldRemoved: Boolean, + accessChanged: Boolean + ): Future { + fun File.isAffected(): Boolean { + if (!isJavaFile()) return false + + if (nameWithoutExtension == ownerClassName.substringAfterLast(".")) return false + + val code = readText() + return code.contains(fieldName) + } + + + val affectedJavaFiles = workDir.walk().filter(File::isAffected).toList() + return FixedFuture(Callbacks.ConstantAffection(affectedJavaFiles)) + } +} + +internal val ProjectDescriptor.allModuleTargets: Collection + get() = buildTargetIndex.allTargets.filterIsInstance() + +private class DependencyDescriptor(val name: String, val exported: Boolean) + +private fun parseDependency(dependency: String): DependencyDescriptor = + DependencyDescriptor(dependency.removeSuffix(EXPORTED_SUFFIX), dependency.endsWith(EXPORTED_SUFFIX)) + +private val EXPORTED_SUFFIX = "[exported]" diff --git a/jps-plugin/jps-tests/test/org/jetbrains/kotlin/jps/build/IncrementalConstantSearchTest.kt.181 b/jps-plugin/jps-tests/test/org/jetbrains/kotlin/jps/build/IncrementalConstantSearchTest.kt.181 new file mode 100644 index 00000000000..fbb7ce3022b --- /dev/null +++ b/jps-plugin/jps-tests/test/org/jetbrains/kotlin/jps/build/IncrementalConstantSearchTest.kt.181 @@ -0,0 +1,43 @@ +/* + * Copyright 2010-2015 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.jps.build + +class IncrementalConstantSearchTest : AbstractIncrementalJpsTest() { + fun testJavaConstantChangedUsedInKotlin() { + doTest("jps-plugin/testData/incremental/custom/javaConstantChangedUsedInKotlin/") + } + + fun testJavaConstantUnchangedUsedInKotlin() { + doTest("jps-plugin/testData/incremental/custom/javaConstantUnchangedUsedInKotlin/") + } + + fun testKotlinConstantChangedUsedInJava() { + doTest("jps-plugin/testData/incremental/custom/kotlinConstantChangedUsedInJava/") + } + + fun testKotlinJvmFieldChangedUsedInJava() { + doTest("jps-plugin/testData/incremental/custom/kotlinJvmFieldChangedUsedInJava/") + } + + fun testKotlinConstantUnchangedUsedInJava() { + doTest("jps-plugin/testData/incremental/custom/kotlinConstantUnchangedUsedInJava/") + } + + fun testKotlinJvmFieldUnchangedUsedInJava() { + doTest("jps-plugin/testData/incremental/custom/kotlinJvmFieldUnchangedUsedInJava/") + } +} \ No newline at end of file diff --git a/jps-plugin/src/META-INF/services/org.jetbrains.jps.builders.java.JavaBuilderExtension.181 b/jps-plugin/src/META-INF/services/org.jetbrains.jps.builders.java.JavaBuilderExtension.181 new file mode 100644 index 00000000000..206b2c9a36d --- /dev/null +++ b/jps-plugin/src/META-INF/services/org.jetbrains.jps.builders.java.JavaBuilderExtension.181 @@ -0,0 +1 @@ +org.jetbrains.kotlin.jps.build.KotlinJavaBuilderExtension \ No newline at end of file diff --git a/jps-plugin/src/org/jetbrains/kotlin/jps/build/KotlinJavaBuilderExtension.kt.181 b/jps-plugin/src/org/jetbrains/kotlin/jps/build/KotlinJavaBuilderExtension.kt.181 new file mode 100644 index 00000000000..63c9e0ba471 --- /dev/null +++ b/jps-plugin/src/org/jetbrains/kotlin/jps/build/KotlinJavaBuilderExtension.kt.181 @@ -0,0 +1,65 @@ +/* + * Copyright 2000-2018 JetBrains s.r.o. 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.jps.build + +import org.jetbrains.jps.api.BasicFuture +import org.jetbrains.jps.builders.java.JavaBuilderExtension +import org.jetbrains.jps.builders.java.dependencyView.Callbacks +import org.jetbrains.jps.incremental.CompileContext +import org.jetbrains.kotlin.incremental.LookupSymbol +import org.jetbrains.kotlin.jps.incremental.withLookupStorage +import java.io.File +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit + +class KotlinJavaBuilderExtension : JavaBuilderExtension() { + override fun getConstantSearch(context: CompileContext): Callbacks.ConstantAffectionResolver { + return KotlinLookupConstantSearch(context) + } +} + +private class KotlinLookupConstantSearch(context: CompileContext) : Callbacks.ConstantAffectionResolver { + private val pool = Executors.newSingleThreadExecutor() + private val dataManager = context.projectDescriptor.dataManager + + override fun request( + ownerClassName: String, + fieldName: String, + accessFlags: Int, + fieldRemoved: Boolean, + accessChanged: Boolean + ): Future { + val future = object : BasicFuture() { + @Volatile + private var result: Callbacks.ConstantAffection = Callbacks.ConstantAffection.EMPTY + + fun result(files: Collection) { + result = Callbacks.ConstantAffection(files) + setDone() + } + + override fun get(): Callbacks.ConstantAffection { + super.get() + return result + } + + override fun get(timeout: Long, unit: TimeUnit): Callbacks.ConstantAffection { + super.get(timeout, unit) + return result + } + } + pool.submit { + if (!future.isCancelled) { + dataManager.withLookupStorage { storage -> + val paths = storage.get(LookupSymbol(name = fieldName, scope = ownerClassName)) + future.result(paths.map { File(it) }) + } + } + } + return future + } +} \ No newline at end of file