diff --git a/eo-parser/src/main/java/org/eolang/parser/ParsingErrors.java b/eo-parser/src/main/java/org/eolang/parser/ParsingErrors.java index 9455938b01..8ca2909983 100644 --- a/eo-parser/src/main/java/org/eolang/parser/ParsingErrors.java +++ b/eo-parser/src/main/java/org/eolang/parser/ParsingErrors.java @@ -26,13 +26,16 @@ import java.util.Iterator; import java.util.LinkedList; import java.util.List; -import org.antlr.v4.runtime.ANTLRErrorListener; +import java.util.Optional; import org.antlr.v4.runtime.BaseErrorListener; +import org.antlr.v4.runtime.NoViableAltException; import org.antlr.v4.runtime.RecognitionException; import org.antlr.v4.runtime.Recognizer; +import org.antlr.v4.runtime.Token; import org.cactoos.Text; import org.cactoos.iterable.Mapped; import org.cactoos.list.ListOf; +import org.cactoos.text.UncheckedText; import org.xembly.Directive; import org.xembly.Directives; @@ -41,8 +44,7 @@ * * @since 0.30.0 */ -final class ParsingErrors extends BaseErrorListener - implements ANTLRErrorListener, Iterable { +final class ParsingErrors extends BaseErrorListener implements Iterable { /** * Errors accumulated. @@ -81,18 +83,41 @@ public void syntaxError( final String msg, final RecognitionException error ) { - this.errors.add( - new ParsingException( - String.format( - "[%d:%d] %s: \"%s\"", - line, position, msg, - // @checkstyle AvoidInlineConditionalsCheck (1 line) - this.lines.size() < line ? "EOF" : this.lines.get(line - 1) - ), - error, - line - ) - ); + // @checkstyle MethodBodyCommentsCheck (20 lines) + // @todo #3332:30min Add more specific error messages. + // Currently we write just "error: no viable alternative at input" for all errors. + // It's better to use 'Recognizer recognizer' parameter of the current method + // to retrieve more specific error messages. + if (error instanceof NoViableAltException) { + final Token token = (Token) symbol; + this.errors.add( + new ParsingException( + String.format( + "[%d:%d] %s:%n%s", + line, position, + "error: no viable alternative at input", + new UnderlinedMessage( + this.line(line).orElse("EOF"), + position, + Math.max(token.getStopIndex() - token.getStartIndex(), 1) + ).formatted() + ), + error, + line + ) + ); + } else { + this.errors.add( + new ParsingException( + String.format( + "[%d:%d] %s: \"%s\"", + line, position, msg, this.line(line).orElse("EOF") + ), + error, + line + ) + ); + } } @Override @@ -121,4 +146,21 @@ public Iterator iterator() { public int size() { return this.errors.size(); } + + /** + * Get the line by number. + * @param number The line number. + * @return The line. + */ + private Optional line(final int number) { + final Optional result; + if (number < 1 || number > this.lines.size()) { + result = Optional.empty(); + } else { + result = Optional.ofNullable(this.lines.get(number - 1)) + .map(UncheckedText::new) + .map(UncheckedText::asString); + } + return result; + } } diff --git a/eo-parser/src/main/java/org/eolang/parser/UnderlinedMessage.java b/eo-parser/src/main/java/org/eolang/parser/UnderlinedMessage.java new file mode 100644 index 0000000000..1e90d53df3 --- /dev/null +++ b/eo-parser/src/main/java/org/eolang/parser/UnderlinedMessage.java @@ -0,0 +1,117 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016-2024 Objectionary.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.eolang.parser; + +import java.util.Collections; + +/** + * Underlined message. + *

+ * For example, if you have a message "Problem is here" and you want to underline + * the word "is", you can create an instance of this class with the following + * parameters: origin="Problem is here", from=8, length=2. + *

+ *

+ * The result will be: + * {@code + * Problem is here + * ^^ + * } + *

+ * @since 0.50 + * @todo #3332:30min Add more decorators for the error message. + * For example, {@link ParsingErrors} currently contains logic related to the message formatting. + * It's better to create a separate class for this purpose. + */ +final class UnderlinedMessage { + + /** + * The message. + */ + private final String origin; + + /** + * The position from which to start underlining. + */ + private final int from; + + /** + * The length of the underline. + */ + private final int length; + + /** + * Ctor. + * @param origin The message. + * @param from The position from which to start underlining. + * @param length The length of the underline. + */ + UnderlinedMessage(final String origin, final int from, final int length) { + this.origin = origin; + this.from = from; + this.length = length; + } + + /** + * Formatted message. + * @return The formatted message. + */ + String formatted() { + return String.format( + "%s\n%s", + this.origin, + this.underline() + ); + } + + /** + * Underline. + * @return The underlined string. + */ + private String underline() { + final String result; + if (this.origin.isEmpty() || this.length <= 0 || this.from >= this.origin.length()) { + result = ""; + } else if (this.from < 0) { + result = UnderlinedMessage.repeat("^", this.origin.length()); + } else { + result = String.format( + "%s%s", + UnderlinedMessage.repeat(" ", this.from), + UnderlinedMessage.repeat("^", Math.min(this.length, this.origin.length())) + ); + } + return result; + } + + /** + * Repeat a symbol n times. + * @param symbol The symbol. + * @param times The number of times to repeat the symbol. + * @return The repeated symbol. + */ + private static String repeat(final String symbol, final int times) { + return String.join("", Collections.nCopies(times, symbol)); + } +} diff --git a/eo-parser/src/test/java/org/eolang/parser/EoSyntaxTest.java b/eo-parser/src/test/java/org/eolang/parser/EoSyntaxTest.java index 5df0e64435..bebf877bdb 100644 --- a/eo-parser/src/test/java/org/eolang/parser/EoSyntaxTest.java +++ b/eo-parser/src/test/java/org/eolang/parser/EoSyntaxTest.java @@ -273,6 +273,17 @@ void checksTypoPacks(final String yaml) { Integer.parseInt(story.after().xpath("/program/errors/error[1]/@line").get(0)), Matchers.equalTo(Integer.parseInt(story.map().get("line").toString())) ); + final String msg = "message"; + if (story.map().containsKey(msg)) { + MatcherAssert.assertThat( + XhtmlMatchers.xhtml(story.after()).toString(), + story.after() + .xpath("/program/errors/error[1]/text()") + .get(0) + .replaceAll("\r", ""), + Matchers.equalTo(story.map().get(msg).toString()) + ); + } } @ParameterizedTest diff --git a/eo-parser/src/test/java/org/eolang/parser/UnderlinedMessageTest.java b/eo-parser/src/test/java/org/eolang/parser/UnderlinedMessageTest.java new file mode 100644 index 0000000000..f7b2215c3d --- /dev/null +++ b/eo-parser/src/test/java/org/eolang/parser/UnderlinedMessageTest.java @@ -0,0 +1,75 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2016-2024 Objectionary.com + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included + * in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.eolang.parser; + +import java.util.stream.Stream; +import org.hamcrest.MatcherAssert; +import org.hamcrest.Matchers; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Test case for {@link UnderlinedMessage}. + * @since 0.50 + * @checkstyle ParameterNumberCheck (500 lines) + */ +@SuppressWarnings("PMD.AvoidDuplicateLiterals") +final class UnderlinedMessageTest { + + @ParameterizedTest + @MethodSource("examples") + void addsUndeline(final String input, final int from, final int length, final String expected) { + MatcherAssert.assertThat( + "We expect the message to be highlighted with underline characters", + new UnderlinedMessage(input, from, length).formatted(), + Matchers.equalTo(expected) + ); + } + + /** + * Test cases for {@link UnderlinedMessageTest#addsUndeline}. + * ANTLR {@link org.antlr.v4.runtime.BaseErrorListener} returns strange line numbers + * and positions like -1. Here I hide this problem intentionally to make all the rest + * tests pass. + * @return Test cases. + */ + private static Stream examples() { + final String issue = "Problem is here"; + return Stream.of( + Arguments.of(issue, 0, 7, "Problem is here\n^^^^^^^"), + Arguments.of(issue, 8, 2, "Problem is here\n ^^"), + Arguments.of(issue, 0, 0, "Problem is here\n"), + Arguments.of(issue, 0, 1, "Problem is here\n^"), + Arguments.of(issue, 14, 1, "Problem is here\n ^"), + Arguments.of(issue, 0, 15, "Problem is here\n^^^^^^^^^^^^^^^"), + Arguments.of(issue, -1, 0, "Problem is here\n"), + Arguments.of(issue, 0, -1, "Problem is here\n"), + Arguments.of(issue, 0, 100, "Problem is here\n^^^^^^^^^^^^^^^"), + Arguments.of(issue, 100, 0, "Problem is here\n"), + Arguments.of(issue, 100, 100, "Problem is here\n"), + Arguments.of("", 1, 10, "\n") + ); + } +} diff --git a/eo-parser/src/test/resources/org/eolang/parser/eo-typos/binding-with-rho.yaml b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/binding-with-rho.yaml index 510fdeb2f8..b857f0e6b8 100644 --- a/eo-parser/src/test/resources/org/eolang/parser/eo-typos/binding-with-rho.yaml +++ b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/binding-with-rho.yaml @@ -21,6 +21,10 @@ # SOFTWARE. --- line: 2 +message: >- + [2:4] error: no viable alternative at input: + y:^ + ^ input: | x y:^ diff --git a/eo-parser/src/test/resources/org/eolang/parser/eo-typos/two-spaces.yaml b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/two-spaces.yaml new file mode 100644 index 0000000000..efe64935b7 --- /dev/null +++ b/eo-parser/src/test/resources/org/eolang/parser/eo-typos/two-spaces.yaml @@ -0,0 +1,36 @@ +# The MIT License (MIT) +# +# Copyright (c) 2016-2024 Objectionary.com +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +--- +line: 5 +message: >- + [5:2] error: no viable alternative at input: + * + ^ +input: | + # This is a code snippet from the following issue: + # https://github.com/objectionary/eo/issues/3332 + [args] > app + seq > @ + * + "a" > a + QQ.io.stdout + a