diff --git a/core/common/src/internal/format/parser/Parser.kt b/core/common/src/internal/format/parser/Parser.kt index a31dab8e..24a1d444 100644 --- a/core/common/src/internal/format/parser/Parser.kt +++ b/core/common/src/internal/format/parser/Parser.kt @@ -48,6 +48,7 @@ internal fun List>.concat(): ParserStructure { } else { ParserStructure(operations, followedBy.map { it.append(other) }) } + fun ParserStructure.simplify(): ParserStructure { val newOperations = mutableListOf>() var currentNumberSpan: MutableList>? = null @@ -122,7 +123,7 @@ internal interface Copyable { } @JvmInline -internal value class Parser>( +internal value class Parser>( private val commands: ParserStructure ) { /** @@ -145,7 +146,7 @@ internal value class Parser>( onSuccess: (Int, Output) -> Unit ) { val parseOptions = mutableListOf(ParserState(initialContainer, commands, startIndex)) - iterate_over_alternatives@while (true) { + iterate_over_alternatives@ while (true) { val state = parseOptions.removeLastOrNull() ?: break val output = state.output.copy() var inputPosition = state.inputPosition @@ -178,14 +179,15 @@ internal value class Parser>( fun match(input: CharSequence, initialContainer: Output, startIndex: Int = 0): Output { val errors = mutableListOf() parse(input, startIndex, initialContainer, allowDanglingInput = false, { errors.add(it) }, { _, out -> return@match out }) + /* + * We do care about **all** parser errors and provide diagnostic information to make the error message approacheable + * for authors of non-trivial formatters with a multitude of potential parsing paths. + * For that, we sort errors so that the most successful parsing paths are at the top, and + * add them all to the parse exception message. + */ errors.sortByDescending { it.position } // `errors` can not be empty because each parser will have (successes + failures) >= 1, and here, successes == 0 - ParseException(errors.first()).let { - for (error in errors.drop(1)) { - it.addSuppressed(ParseException(error)) - } - throw it - } + throw ParseException(errors) } fun matchOrNull(input: CharSequence, initialContainer: Output, startIndex: Int = 0): Output? { @@ -200,4 +202,18 @@ internal value class Parser>( ) } -internal class ParseException(error: ParseError) : Exception("Position ${error.position}: ${error.message()}") +internal class ParseException(errors: List) : Exception(formatError(errors)) + +private fun formatError(errors: List): String { + if (errors.size == 1) { + return "Position ${errors[0].position}: ${errors[0].message()}" + } + // 20 For average error string: "Expected X but got Y" + // 13 for static part "Position :," + val averageMessageLength = 20 + 13 + return errors.joinTo( + StringBuilder(averageMessageLength * errors.size), + prefix = "Errors: ", + separator = ", " + ) { "position ${it.position}: '${it.message()}'" }.toString() +}