diff --git a/compiler/frontend.script/src/org/jetbrains/kotlin/script/ScriptContentLoader.kt b/compiler/frontend.script/src/org/jetbrains/kotlin/script/ScriptContentLoader.kt index dbfaaec0f9a..9d8c657cf39 100644 --- a/compiler/frontend.script/src/org/jetbrains/kotlin/script/ScriptContentLoader.kt +++ b/compiler/frontend.script/src/org/jetbrains/kotlin/script/ScriptContentLoader.kt @@ -25,8 +25,10 @@ import org.jetbrains.kotlin.psi.KtAnnotationEntry import org.jetbrains.kotlin.psi.KtFile import java.io.File import kotlin.reflect.KClass +import kotlin.script.dependencies.DependenciesResolver.ResolveResult.Failure import kotlin.script.dependencies.ScriptContents import kotlin.script.dependencies.ScriptDependencies +import kotlin.script.dependencies.ScriptReport class ScriptContentLoader(private val project: Project) { fun getScriptContents(scriptDefinition: KotlinScriptDefinition, file: VirtualFile) @@ -62,10 +64,16 @@ class ScriptContentLoader(private val project: Project) { file: VirtualFile ): ScriptDependencies? { val scriptContents = getScriptContents(scriptDef, file) - val result = scriptDef.dependencyResolver.resolve( - scriptContents, - getEnvironment(scriptDef) - ) + val environment = getEnvironment(scriptDef) + val result = try { + scriptDef.dependencyResolver.resolve( + scriptContents, + environment + ) + } + catch (e: Throwable) { + e.asResolveFailure(scriptDef) + } ServiceManager.getService(project, ScriptReportSink::class.java)?.attachReports(file, result.reports) return result.dependencies?.adjustByDefinition(scriptDef) } @@ -82,3 +90,7 @@ fun ScriptDependencies.adjustByDefinition( return copy(classpath = additionalClasspath + classpath) } +fun Throwable.asResolveFailure(scriptDef: KotlinScriptDefinition): Failure { + val prefix = "${scriptDef.dependencyResolver::class.simpleName} threw exception ${this::class.simpleName}:\n " + return Failure(ScriptReport(prefix + (message ?: ""))) +} \ No newline at end of file diff --git a/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTemplateTest.kt b/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTemplateTest.kt index d29dcf9f137..ef8c6a5cf2e 100644 --- a/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTemplateTest.kt +++ b/compiler/tests/org/jetbrains/kotlin/scripts/ScriptTemplateTest.kt @@ -290,6 +290,14 @@ class ScriptTemplateTest { Assert.assertNotNull(aClass) } + @Test + fun testThrowing() { + val messageCollector = TestMessageCollector() + compileScript("fib.kts", ScriptWithThrowingResolver::class, null, messageCollector = messageCollector) + + messageCollector.assertHasMessage("Exception from resolver", desiredSeverity = CompilerMessageSeverity.ERROR) + } + @Test fun testSmokeScriptException() { val aClass = compileScript("smoke_exception.kts", ScriptWithArrayParam::class) @@ -511,6 +519,12 @@ class SeveralConstructorsResolver(val c: Int): TestKotlinScriptDependenciesResol } class DefaultArgsConstructorResolver(val c: Int = 0): TestKotlinScriptDependenciesResolver() +class ThrowingResolver: DependenciesResolver { + override fun resolve(scriptContents: ScriptContents, environment: Environment): ResolveResult { + throw IllegalStateException("Exception from resolver") + } +} + @ScriptTemplateDefinition( scriptFilePattern =".*\\.kts", resolver = TestKotlinScriptDummyDependenciesResolver::class) @@ -583,6 +597,9 @@ abstract class ScriptWithSeveralConstructorsResolver(val num: Int) @ScriptTemplateDefinition(resolver = DefaultArgsConstructorResolver::class) abstract class ScriptWithDefaultArgsResolver(val num: Int) +@ScriptTemplateDefinition(resolver = ThrowingResolver::class) +abstract class ScriptWithThrowingResolver(val num: Int) + @Target(AnnotationTarget.FILE) @Retention(AnnotationRetention.RUNTIME) annotation class DependsOn(val path: String) diff --git a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDependenciesUpdater.kt b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDependenciesUpdater.kt index 448eb6e1397..d2cddecef30 100644 --- a/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDependenciesUpdater.kt +++ b/idea/idea-core/src/org/jetbrains/kotlin/idea/core/script/ScriptDependenciesUpdater.kt @@ -28,6 +28,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import com.intellij.openapi.vfs.newvfs.BulkFileListener import com.intellij.openapi.vfs.newvfs.events.VFileEvent +import kotlinx.coroutines.experimental.CoroutineDispatcher import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.asCoroutineDispatcher import kotlinx.coroutines.experimental.launch @@ -135,40 +136,63 @@ internal class ScriptDependenciesUpdater( ): TimeStampedJob { val currentTimeStamp = TimeStamps.next() - fun process(result: DependenciesResolver.ResolveResult) { - val lastRequest = requests[file.path] - val lastTimeStamp = lastRequest?.job?.timeStamp - val isLastSentRequest = lastTimeStamp == null || lastTimeStamp == currentTimeStamp - if (isLastSentRequest) { - if (lastRequest != null) { - // no job running atm unless there is a job started while we process this result - requests.replace(file.path, lastRequest, ModStampedRequest(lastRequest.modificationStamp, job = null)) - } - ServiceManager.getService(project, ScriptReportSink::class.java)?.attachReports(file, result.reports) - val resultingDependencies = (result.dependencies ?: ScriptDependencies.Empty).adjustByDefinition(scriptDef) - if (cache(resultingDependencies, file)) { - onChange() - } - } - } - val dependenciesResolver = scriptDef.dependencyResolver val scriptContents = contentLoader.getScriptContents(scriptDef, file) val environment = contentLoader.getEnvironment(scriptDef) val newJob = if (dependenciesResolver is AsyncDependenciesResolver) { - launch(asyncUpdatesDispatcher) { - process(dependenciesResolver.resolveAsync(scriptContents, environment)) + launchAsyncUpdate(asyncUpdatesDispatcher, file, currentTimeStamp, scriptDef) { + dependenciesResolver.resolveAsync(scriptContents, environment) } } else { assert(dependenciesResolver is LegacyResolverWrapper) - launch(legacyUpdatesDispatcher) { - process(dependenciesResolver.resolve(scriptContents, environment)) + launchAsyncUpdate(legacyUpdatesDispatcher, file, currentTimeStamp, scriptDef) { + dependenciesResolver.resolve(scriptContents, environment) } } return TimeStampedJob(newJob, currentTimeStamp) } + private fun launchAsyncUpdate( + dispatcher: CoroutineDispatcher, + file: VirtualFile, + currentTimeStamp: TimeStamp, + scriptDef: KotlinScriptDefinition, + doResolve: suspend () -> DependenciesResolver.ResolveResult + ) = launch(dispatcher) { + val result = try { + doResolve() + } + catch (t: Throwable) { + t.asResolveFailure(scriptDef) + } + + processResult(file, currentTimeStamp, result, scriptDef) + } + + private fun processResult( + file: VirtualFile, + currentTimeStamp: TimeStamp, + result: DependenciesResolver.ResolveResult, + scriptDef: KotlinScriptDefinition + ) { + val lastRequest = requests[file.path] + val lastTimeStamp = lastRequest?.job?.timeStamp + val isLastSentRequest = lastTimeStamp == null || lastTimeStamp == currentTimeStamp + if (isLastSentRequest) { + if (lastRequest != null) { + // no job running atm unless there is a job started while we process this result + requests.replace(file.path, lastRequest, ModStampedRequest(lastRequest.modificationStamp, job = null)) + } + ServiceManager.getService(project, ScriptReportSink::class.java)?.attachReports(file, result.reports) + val resultingDependencies = (result.dependencies ?: ScriptDependencies.Empty).adjustByDefinition(scriptDef) + if (cache(resultingDependencies, file)) { + onChange() + } + } + } + + fun updateSync(file: VirtualFile, scriptDef: KotlinScriptDefinition): Boolean { val newDeps = contentLoader.loadContentsAndResolveDependencies(scriptDef, file) ?: ScriptDependencies.Empty return cache(newDeps, file) diff --git a/idea/testData/script/definition/highlighting/throwingResolver/script.kts b/idea/testData/script/definition/highlighting/throwingResolver/script.kts new file mode 100644 index 00000000000..3b227722bec --- /dev/null +++ b/idea/testData/script/definition/highlighting/throwingResolver/script.kts @@ -0,0 +1,5 @@ + + +val s = 3 +val g = 4 \ No newline at end of file diff --git a/idea/testData/script/definition/highlighting/throwingResolver/template/template.kt b/idea/testData/script/definition/highlighting/throwingResolver/template/template.kt new file mode 100644 index 00000000000..712cc56324c --- /dev/null +++ b/idea/testData/script/definition/highlighting/throwingResolver/template/template.kt @@ -0,0 +1,18 @@ +package custom.scriptDefinition + +import java.io.File +import kotlin.script.dependencies.* +import kotlin.script.dependencies.experimental.* +import kotlin.script.templates.ScriptTemplateDefinition + +class TestDependenciesResolver : DependenciesResolver { + override fun resolve( + scriptContents: ScriptContents, + environment: Environment + ): DependenciesResolver.ResolveResult { + error("Exception from resolver") + } +} + +@ScriptTemplateDefinition(TestDependenciesResolver::class, scriptFilePattern = "script.kts") +open class Template \ No newline at end of file diff --git a/idea/tests/org/jetbrains/kotlin/idea/script/ScriptConfigurationHighlightingTestGenerated.java b/idea/tests/org/jetbrains/kotlin/idea/script/ScriptConfigurationHighlightingTestGenerated.java index 3c8b406425e..4a44fb98cf0 100644 --- a/idea/tests/org/jetbrains/kotlin/idea/script/ScriptConfigurationHighlightingTestGenerated.java +++ b/idea/tests/org/jetbrains/kotlin/idea/script/ScriptConfigurationHighlightingTestGenerated.java @@ -95,4 +95,10 @@ public class ScriptConfigurationHighlightingTestGenerated extends AbstractScript String fileName = KotlinTestUtils.navigationMetadata("idea/testData/script/definition/highlighting/simple/"); doTest(fileName); } + + @TestMetadata("throwingResolver") + public void testThrowingResolver() throws Exception { + String fileName = KotlinTestUtils.navigationMetadata("idea/testData/script/definition/highlighting/throwingResolver/"); + doTest(fileName); + } }