kotlin.script.experimental.api.errorHandling.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kotlin-main-kts Show documentation
Show all versions of kotlin-main-kts Show documentation
Kotlin "main" script definition
/*
* Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
@file:Suppress("unused")
package kotlin.script.experimental.api
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.PrintStream
import java.io.Serializable
/**
* The single script diagnostic report
* @param code diagnostic identifier
* @param message diagnostic message
* @param severity diagnostic severity ({@link ScriptDiagnostic#Severity})
* @param location optional source location for the diagnostic
* @param exception optional exception caused the diagnostic
*/
data class ScriptDiagnostic(
val code: Int,
val message: String,
val severity: Severity = Severity.ERROR,
val sourcePath: String? = null,
val location: SourceCode.Location? = null,
val exception: Throwable? = null
) : Serializable {
/**
* The diagnostic severity
*/
enum class Severity { DEBUG, INFO, WARNING, ERROR, FATAL }
constructor(
code: Int,
message: String,
severity: Severity = Severity.ERROR,
locationWithId: SourceCode.LocationWithId?,
exception: Throwable? = null
) : this(code, message, severity, locationWithId?.codeLocationId, locationWithId?.locationInText, exception)
override fun toString(): String = render()
/**
* Render diagnostics message as a string in a form:
* "[SEVERITY ]message[ (file:line:column)][: exception message[\n exception stacktrace]]"
* @param withSeverity add severity prefix, true by default
* @param withLocation add error location in the compiled script, if present, true by default
* @param withException add exception message, if present, true by default
* @param withStackTrace add exception stacktrace, if exception is present and [withException] is true, false by default
*/
fun render(
withSeverity: Boolean = true,
withLocation: Boolean = true,
withException: Boolean = true,
withStackTrace: Boolean = false
): String = buildString {
if (withSeverity) {
append(severity.name)
append(' ')
}
append(message)
if (withLocation && (sourcePath != null || location != null)) {
append(" (")
sourcePath?.let { append(it.substringAfterLast(File.separatorChar)) }
location?.let {
append(':')
append(it.start.line)
append(':')
append(it.start.col)
}
append(')')
}
if (withException && exception != null) {
append(": ")
append(exception)
if (withStackTrace) {
ByteArrayOutputStream().use { os ->
val ps = PrintStream(os)
exception.printStackTrace(ps)
ps.flush()
append("\n")
append(os.toString())
}
}
}
}
companion object {
private const val serialVersionUID: Long = 0L
const val unspecifiedInfo = 0
const val unspecifiedError = -1
const val unspecifiedException = -2
const val incompleteCode = -3
}
}
fun ScriptDiagnostic.isError() =
(severity == ScriptDiagnostic.Severity.ERROR || severity == ScriptDiagnostic.Severity.FATAL) &&
(code == ScriptDiagnostic.unspecifiedException || code == ScriptDiagnostic.unspecifiedError)
/**
* The result wrapper with diagnostics container
*/
sealed class ResultWithDiagnostics {
/**
* The diagnostic reports container
*/
abstract val reports: List
/**
* The successful [value] result with optional [reports] with diagnostics
*/
data class Success(
val value: R,
override val reports: List = listOf()
) : ResultWithDiagnostics()
/**
* The class representing the failure result
* @param reports diagnostics associated with the failure
*/
data class Failure(
override val reports: List
) : ResultWithDiagnostics() {
constructor(vararg reports: ScriptDiagnostic) : this(reports.asList())
}
}
/**
* Chains actions on successful result:
* If receiver is success - executes [body] and merge diagnostic reports
* otherwise returns the failure as is
*/
inline fun ResultWithDiagnostics.onSuccess(body: (R1) -> ResultWithDiagnostics): ResultWithDiagnostics =
when (this) {
is ResultWithDiagnostics.Success -> this.reports + body(this.value)
is ResultWithDiagnostics.Failure -> this
}
/**
* maps transformation ([body]) over iterable merging diagnostics
* return failure with merged diagnostics after first failed transformation
* and success with merged diagnostics and list of results if all transformations succeeded
*/
inline fun Iterable.mapSuccess(body: (T) -> ResultWithDiagnostics): ResultWithDiagnostics> =
mapSuccessImpl(body) { results, r ->
results.add(r)
}
/**
* maps transformation ([body]) over iterable merging diagnostics
* return failure with merged diagnostics after first failed transformation
* and success with merged diagnostics and list of not null results if all transformations succeeded
*/
inline fun Iterable.mapNotNullSuccess(body: (T) -> ResultWithDiagnostics): ResultWithDiagnostics> =
mapSuccessImpl(body) { results, r ->
if (r != null)
results.add(r)
}
/**
* maps transformation ([body]) over iterable merging diagnostics and flatten the results
* return failure with merged diagnostics after first failed transformation
* and success with merged diagnostics and list of results if all transformations succeeded
*/
inline fun Iterable.flatMapSuccess(body: (T) -> ResultWithDiagnostics>): ResultWithDiagnostics> =
mapSuccessImpl(body) { results, r ->
results.addAll(r)
}
inline fun Iterable.mapSuccessImpl(body: (T) -> ResultWithDiagnostics, updateResults: (MutableList, R1) -> Unit): ResultWithDiagnostics> {
val reports = ArrayList()
val results = ArrayList()
for (it in this) {
val result = body(it)
reports.addAll(result.reports)
when (result) {
is ResultWithDiagnostics.Success -> {
updateResults(results, result.value)
}
else -> {
return ResultWithDiagnostics.Failure(reports)
}
}
}
return results.asSuccess(reports)
}
/**
* Chains actions on failure:
* If receiver is failure - executed [body]
* otherwise returns the receiver as is
*/
inline fun ResultWithDiagnostics.onFailure(body: (ResultWithDiagnostics) -> Unit): ResultWithDiagnostics {
if (this is ResultWithDiagnostics.Failure) {
body(this)
}
return this
}
/**
* Merges diagnostics report with the [result] wrapper
*/
operator fun List.plus(result: ResultWithDiagnostics): ResultWithDiagnostics = when (result) {
is ResultWithDiagnostics.Success -> ResultWithDiagnostics.Success(result.value, this + result.reports)
is ResultWithDiagnostics.Failure -> ResultWithDiagnostics.Failure(this + result.reports)
}
/**
* Converts the receiver value to the Success result wrapper with optional diagnostic [reports]
*/
fun R.asSuccess(reports: List = listOf()): ResultWithDiagnostics.Success =
ResultWithDiagnostics.Success(this, reports)
/**
* Makes Failure result with optional diagnostic [reports]
*/
fun makeFailureResult(reports: List): ResultWithDiagnostics.Failure =
ResultWithDiagnostics.Failure(reports)
/**
* Makes Failure result with optional diagnostic [reports]
*/
fun makeFailureResult(vararg reports: ScriptDiagnostic): ResultWithDiagnostics.Failure =
ResultWithDiagnostics.Failure(reports.asList())
/**
* Makes Failure result with diagnostic [message] with optional [path] and [location]
*/
fun makeFailureResult(message: String, path: String? = null, location: SourceCode.Location? = null): ResultWithDiagnostics.Failure =
ResultWithDiagnostics.Failure(message.asErrorDiagnostics(ScriptDiagnostic.unspecifiedError, path, location))
/**
* Makes Failure result with diagnostic [message] with optional [locationWithId]
*/
fun makeFailureResult(message: String, locationWithId: SourceCode.LocationWithId?): ResultWithDiagnostics.Failure =
ResultWithDiagnostics.Failure(message.asErrorDiagnostics(ScriptDiagnostic.unspecifiedError, locationWithId))
/**
* Converts the receiver Throwable to the Failure results wrapper with optional [customMessage], [path] and [location]
*/
fun Throwable.asDiagnostics(
code: Int = ScriptDiagnostic.unspecifiedException,
customMessage: String? = null,
path: String? = null,
location: SourceCode.Location? = null,
severity: ScriptDiagnostic.Severity = ScriptDiagnostic.Severity.ERROR
): ScriptDiagnostic =
ScriptDiagnostic(code, customMessage ?: message ?: "$this", severity, path, location, this)
/**
* Converts the receiver Throwable to the Failure results wrapper with optional [customMessage], [locationWithId]
*/
fun Throwable.asDiagnostics(
code: Int = ScriptDiagnostic.unspecifiedException,
customMessage: String? = null,
locationWithId: SourceCode.LocationWithId?,
severity: ScriptDiagnostic.Severity = ScriptDiagnostic.Severity.ERROR
): ScriptDiagnostic =
ScriptDiagnostic(code, customMessage ?: message ?: "$this", severity, locationWithId, this)
/**
* Converts the receiver String to error diagnostic report with optional [path] and [location]
*/
fun String.asErrorDiagnostics(
code: Int = ScriptDiagnostic.unspecifiedError,
path: String? = null,
location: SourceCode.Location? = null
): ScriptDiagnostic =
ScriptDiagnostic(code, this, ScriptDiagnostic.Severity.ERROR, path, location)
/**
* Converts the receiver String to error diagnostic report with optional [locationWithId]
*/
fun String.asErrorDiagnostics(
code: Int = ScriptDiagnostic.unspecifiedError,
locationWithId: SourceCode.LocationWithId?
): ScriptDiagnostic =
ScriptDiagnostic(code, this, ScriptDiagnostic.Severity.ERROR, locationWithId)
/**
* Extracts the result value from the receiver wrapper or null if receiver represents a Failure
*/
fun ResultWithDiagnostics.valueOrNull(): R? = when (this) {
is ResultWithDiagnostics.Success -> value
else -> null
}
/**
* Extracts the result value from the receiver wrapper or run non-returning lambda if receiver represents a Failure
*/
inline fun ResultWithDiagnostics.valueOr(body: (ResultWithDiagnostics.Failure) -> Nothing): R = when (this) {
is ResultWithDiagnostics.Success -> value
is ResultWithDiagnostics.Failure -> body(this)
}
/**
* Extracts the result value from the receiver wrapper or throw RuntimeException with diagnostics
*/
fun ResultWithDiagnostics.valueOrThrow(): R = valueOr {
throw RuntimeException(
reports.joinToString("\n") { it.exception?.toString() ?: it.message },
reports.find { it.exception != null }?.exception
)
}
class IterableResultsCollector {
private val diagnostics = mutableListOf()
private val failureResults = mutableListOf>>()
private val values = mutableListOf()
fun add(result: ResultWithDiagnostics>) {
if (result is ResultWithDiagnostics.Success) {
diagnostics.addAll(result.reports)
values.addAll(result.value)
} else {
failureResults.add(result)
}
}
fun addValue(value: T) {
values.add(value)
}
fun addDiagnostic(diagnostic: ScriptDiagnostic) {
diagnostics.add(diagnostic)
}
fun getResult(): ResultWithDiagnostics> {
return if (values.isEmpty()) {
ResultWithDiagnostics.Failure(diagnostics + failureResults.flatMap { it.reports })
} else {
ResultWithDiagnostics.Success(values, diagnostics)
}
}
}
fun Iterable>>.asSuccessIfAny(): ResultWithDiagnostics> {
return IterableResultsCollector().run {
forEach { add(it) }
getResult()
}
}