Catch and report exceptions from DependenciesResolver

This commit is contained in:
Pavel V. Talanov
2017-07-18 17:07:50 +03:00
parent eb5be038fa
commit 5faad493b4
6 changed files with 107 additions and 25 deletions
@@ -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 message>")))
}
@@ -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)
@@ -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)
@@ -0,0 +1,5 @@
<error descr="TestDependenciesResolver threw exception IllegalStateException:
Exception from resolver">
</error>
val s = 3
val g = 4
@@ -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
@@ -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);
}
}