[
https://issues.apache.org/jira/browse/GROOVY-12353?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18111794#comment-18111794
]
ASF GitHub Bot commented on GROOVY-12353:
-----------------------------------------
Copilot commented on code in PR #2877:
URL: https://github.com/apache/groovy/pull/2877#discussion_r3939441663
##########
src/main/java/org/apache/groovy/parser/antlr4/GroovySyntaxError.java:
##########
@@ -19,7 +19,14 @@
package org.apache.groovy.parser.antlr4;
/**
- * Represents a syntax error of groovy program
+ * Represents a syntax error of a Groovy program, raised by the lexer or
parser.
+ * <p>
+ * The message is the diagnostic as produced by the recogniser (for example
+ * {@code Unclosed string literal} or {@code Unexpected character:
'\u005cu200b'}).
+ * Line and column are 1-based; they are the caret location, not part of the
+ * message string — {@link org.codehaus.groovy.syntax.SyntaxException} appends
+ * {@code @ line N, column M} when the error is reported through the compiler.
Review Comment:
This class-wide statement conflicts with the public
`SyntaxErrorReportable.throwSyntaxError(..., true)` path, which deliberately
appends `PositionInfo` to the `GroovySyntaxError` message. Please qualify this
as the compiler's normal lexer path rather than claiming the location can never
be part of the message.
##########
src/main/java/org/apache/groovy/parser/antlr4/internal/AbstractFriendlyErrorStrategy.java:
##########
@@ -49,24 +77,234 @@ abstract class AbstractFriendlyErrorStrategy extends
DefaultErrorStrategy {
}
/**
- * Prefer a precise "Missing …" delimiter diagnostic when the token stream
- * clearly indicates an unclosed / incomplete construct; otherwise fall
- * back to the generic message.
+ * Prefer a relocated "Missing …" closer when the token stream supports it;
+ * otherwise refine the generic {@code Unexpected input: ...} fallback
+ * (reserved keyword, sole expected punctuation, unexpected EOF).
+ * Locate/refine run in a defensive try; a single listener dispatch is
+ * outside so a listener {@link IllegalArgumentException} /
+ * {@link IndexOutOfBoundsException} is not mistaken for a lookup failure
+ * and re-dispatched.
*/
private void reportFriendlyError(final Parser recognizer, final
RecognitionException e, final String fallbackMessage) {
- MissingDelimiterDiagnostic.Hit hit = null;
+ Token at = e.getOffendingToken();
+ String message = fallbackMessage;
try {
- // Incomplete / synthetic contexts can leave token indices out of
range.
- hit =
MissingDelimiterDiagnostic.locate(recognizer.getInputStream(), e);
+ // Incomplete / synthetic contexts can leave token indices out of
range,
+ // and getExpectedTokens() can reject an invalid ATN state number.
+ MissingDelimiterDiagnostic.Hit hit =
MissingDelimiterDiagnostic.locate(recognizer.getInputStream(), e);
+ if (hit != null) {
+ at = hit.at;
+ message = hit.message;
+ } else {
+ message = refineFallbackMessage(e, fallbackMessage);
+ }
} catch (IndexOutOfBoundsException | IllegalArgumentException ignored)
{
- // Fall through to the generic message. Catch only locate()'s known
- // defensive failures — never listener-side fatals (e.g.
addFatalError).
+ message = fallbackMessage;
+ }
+ recognizer.notifyErrorListeners(at, message, e);
+ }
+
+ /**
+ * Improve a generic mismatch/NVAE sentence without moving the caret.
+ * Precedence: reserved/misplaced keyword, then a singleton expected
+ * punctuation token, then unexpected EOF, then {@code generic}.
+ */
+ static String refineFallbackMessage(final RecognitionException e, final
String generic) {
+ if (e == null) {
+ return generic;
+ }
+ Token offending = e.getOffendingToken();
+ if (offending != null) {
+ String keyword = keywordMessage(offending.getType());
+ if (keyword != null) {
+ return keyword;
+ }
+ String misplacedDefault = misplacedDefaultClause(e, offending);
+ if (misplacedDefault != null) {
+ return misplacedDefault;
+ }
+ String safeIndex = unexpectedSafeIndex(e, offending);
+ if (safeIndex != null) {
+ return safeIndex;
+ }
+ }
+ String sole = soleExpectedMessage(e);
+ if (sole != null) {
+ return sole;
+ }
+ if (offending != null && offending.getType() == Token.EOF) {
+ return "Unexpected end of input";
}
- if (hit != null) {
- recognizer.notifyErrorListeners(hit.at, hit.message, e);
- return;
+ return generic;
+ }
+
+ /**
+ * javac wording for reserved keywords Groovy tokenises but does not
+ * implement, and for control-flow keywords that appear as the offending
+ * token. Like javac, the sentence names the keyword even when a related
+ * construct is nearby but incomplete ({@code if (x) else {}} is
+ * {@code 'else' without 'if'} because the then-branch is missing).
+ * {@code const} is reserved and unused in Java (JLS 3.9); the replacements
+ * are {@code val} (locals; preferred over {@code final} in Groovy 6) and
+ * {@code static final} (class constants). {@code threadsafe} is reserved
+ * and unused in Groovy. {@code default} itself is <em>not</em> mapped
+ * here: it is a valid interface-method and annotation-element keyword, so
+ * an offending {@code default} (for example {@code def m() default {1}})
+ * is not "outside of switch". {@code default:} / {@code default ->}
+ * outside a switch is recognised via {@link #misplacedDefaultClause}.
+ */
+ static String keywordMessage(final int tokenType) {
+ return switch (tokenType) {
+ case CONST -> "'const' is not supported; use 'val' or 'static
final' instead";
+ case GOTO -> "'goto' is not supported";
+ case THREADSAFE -> "'threadsafe' is not supported";
+ case ELSE -> "'else' without 'if'";
+ case CATCH -> "'catch' without 'try'";
+ case FINALLY -> "'finally' without 'try'";
+ case CASE -> "'case' outside of switch";
+ default -> null;
+ };
+ }
+
+ /**
+ * {@code default: x} / {@code default -> x} at script or method scope:
+ * ANTLR's offending token is the {@code :} or {@code ->}, so
+ * {@link #keywordMessage(int)} on the offender is silent. Look one
+ * default-channel token back; skip {@code NL} so a line break between
+ * {@code default} and the clause marker still names {@code default}.
+ * Restricted to those two markers so {@code interface I { default }}
+ * (offender {@code }}) is not mislabelled.
+ */
+ static String misplacedDefaultClause(final RecognitionException e, final
Token offending) {
+ if (e == null || offending == null) {
+ return null;
+ }
+ int type = offending.getType();
+ if (type != COLON && type != ARROW) {
+ return null;
+ }
+ if (!(e.getInputStream() instanceof TokenStream tokens)) {
+ return null;
+ }
+ int index = offending.getTokenIndex();
+ if (index < 1) {
+ return null;
+ }
+ try {
+ for (int i = index - 1; i >= 0; i--) {
+ Token prev = tokens.get(i);
+ int prevType = prev.getType();
+ if (prevType == NL || prev.getChannel() !=
Token.DEFAULT_CHANNEL) {
+ continue;
+ }
+ return prevType == DEFAULT ? "'default' outside of switch" :
null;
+ }
+ } catch (IndexOutOfBoundsException | IllegalArgumentException ignored)
{
+ return null;
}
- notifyErrorListeners(recognizer, fallbackMessage, e);
+ return null;
+ }
+
+ /**
+ * {@code ?[} is a single token (Groovy 4 safe index) and must follow an
+ * expression. A leading {@code ?[0]} or {@code a??[0]} (ternary {@code ?}
+ * then {@code ?[}) has {@code ?[} as the offender; name it rather than
+ * dumping a generic {@code Unexpected input} span.
+ */
+ static String unexpectedSafeIndex(final RecognitionException e, final
Token offending) {
+ if (e == null || offending == null || offending.getType() !=
SAFE_INDEX) {
+ return null;
+ }
+ if (!(e.getInputStream() instanceof TokenStream tokens)) {
+ return "'?[' requires an expression before it";
+ }
+ int index = offending.getTokenIndex();
+ if (index < 1) {
+ return "'?[' requires an expression before it";
+ }
+ try {
+ for (int i = index - 1; i >= 0; i--) {
+ Token prev = tokens.get(i);
+ int prevType = prev.getType();
+ if (prevType == NL || prev.getChannel() !=
Token.DEFAULT_CHANNEL) {
+ continue;
+ }
+ // postfix-able previous token: this '?[' is in a
pairing/content
+ // failure, not a missing receiver — leave the generic
sentence.
+ if (prevType == QUESTION) {
+ return "'?[' requires an expression before it";
+ }
+ return null;
Review Comment:
This treats every preceding token other than `QUESTION` as if it could end
an expression. For example, in `foo(?[0])` or `x = ?[0]`, the previous token is
`LPAREN` or `ASSIGN`; `SAFE_INDEX` cannot start a primary (it only appears as
an `indexPropertyArgs` suffix), but this returns `null` and leaves the generic
diagnostic. Please return the missing-receiver message unless the previous
token can actually terminate an expression, and cover at least one
operator/delimiter case end to end.
> Improve remaining common syntax error messages (unclosed literals, unexpected
> characters, missing punctuation, reserved keywords)
> ---------------------------------------------------------------------------------------------------------------------------------
>
> Key: GROOVY-12353
> URL: https://issues.apache.org/jira/browse/GROOVY-12353
> Project: Groovy
> Issue Type: Improvement
> Reporter: Daniel Sun
> Priority: Major
>
> h3. Problem
> After GROOVY-12169 and GROOVY-12171, several everyday syntax mistakes still
> produce a generic *Unexpected input* or *Unexpected character* message that
> does not name the actual problem.
> Invisible or look-alike characters (zero-width space, BOM, NUL, no-break
> space, curly quotes, non-ASCII dashes) render as an empty or misleading glyph
> between quotes. Unclosed quotes and comments are reported as an unexpected
> quote or slash rather than as an unclosed literal. {{if true}} without
> parentheses, {{x ? y}}, and {{const x = 1}} look like random unexpected
> tokens instead of a missing {{(}} / {{:}} or an unimplemented keyword. Groovy
> 4 safe index is a single token that pairs with a closing bracket, but an
> unclosed index inside parentheses was reported as {{Missing ')'}}.
> h3. Examples (before)
> * Unclosed string:
> {noformat}
> println 'Hello
> {noformat}
> reports:
> {noformat}
> Unexpected character: ''' @ line 1, column 9.
> {noformat}
> (caret on the opening quote)
> * Unclosed block comment:
> {noformat}
> /* comment
> {noformat}
> reports:
> {noformat}
> Unexpected input: '/'
> {noformat}
> * Invisible character ({{def}} + U+200B + {{name = null}}):
> {noformat}
> Unexpected character: ''
> {noformat}
> * Missing punctuation:
> {noformat}
> if true { x = 1 }
> {noformat}
> reports:
> {noformat}
> Unexpected input: 'true'
> {noformat}
> * Reserved keyword:
> {noformat}
> const x = 1
> {noformat}
> reports:
> {noformat}
> Unexpected input: 'const'
> {noformat}
> * Duplicate location:
> {noformat}
> def n = 1_
> {noformat}
> reports:
> {noformat}
> Number ending with underscores is invalid @ line 1, column 10 @ line 1,
> column 10.
> {noformat}
> * Hard-coded varargs name:
> {noformat}
> def m(int... a, int b) {}
> {noformat}
> reports:
> {noformat}
> The var-arg parameter strs must be the last parameter
> {noformat}
> * Unclosed safe index inside parentheses:
> {noformat}
> (a?[0
> {noformat}
> reports:
> {noformat}
> Missing ')'
> {noformat}
> h3. Root cause
> * Lexer {{UNEXPECTED_CHAR}} inlined the raw character with only a
> quote-escape, so control, format, and look-alike characters vanish or
> mislead. An unexpected quote is almost always an unclosed string, but the
> message never said so.
> * Unclosed block comments failed the comment rule and were retokenized as
> {{/}}, so the parser saw an unexpected slash.
> * Parser fallback wording was still ANTLR's *Unexpected input*, even when the
> expected set was a single punctuation token, or the offender was a reserved
> or misplaced keyword ({{const}}, {{goto}}, {{threadsafe}}, {{else}},
> {{catch}}, {{finally}}, {{case}}) or EOF. For {{default:}} / {{default ->}}
> outside a switch, the offender is {{:}} or {{->}}, not {{default}}.
> * The missing-closer diagnostic trusted a sole expected closer. For an
> unclosed safe index inside parentheses the parser expected {{)}}, so the
> inner unclosed index was reported as {{Missing ')'}}. The safe-index token
> was not treated as the same opener family as a normal index.
> * Lexer {{require(..., true)}} appended {{@ line N, column M}} to
> {{GroovySyntaxError}}, and {{SyntaxException}} appended the same location
> again.
> * {{AstBuilder}} used a hard-coded parameter name {{strs}} in the
> varargs-not-last diagnostic.
> Grammar-level parser error alternatives are not an option: GROOVY-9588 showed
> they enlarge the ATN and slow successful parses. An EOF closer on slashy
> strings is also not an option: {{/}} after an expression with newlines is
> division, not an unclosed slashy string.
> h3. Goal
> Give javac-aligned, developer-facing sentences for these common mistakes,
> with an accurate caret, without reintroducing parser error alternatives on
> the hot path.
> h3. Approach
> Error-path-only, two layers. Successful parses never enter these helpers.
> * Lexer ({{GroovyLexer.g4}} / {{AbstractLexer}}): an unexpected quote becomes
> *Unclosed string literal*; an unclosed block comment becomes *Unclosed
> comment* at the opener (one non-greedy loop, then the closer or EOF — not a
> second lexer rule, which would win by longest match and swallow trailing
> source). Other unexpected characters go through {{getCharErrorDisplay}}
> (shared with the GString {{$}} path from GROOVY-12171). Unicode spaces other
> than U+0020, curly quotes, and non-ASCII dashes are named as a Unicode escape
> (for example {{'\u200b'}}) so they do not vanish into the caret line. Stop
> attaching position text on lexer {{require}} calls so {{SyntaxException}} is
> the only source of {{@ line N, column M}}.
> * Parser ({{AbstractFriendlyErrorStrategy}}): {{MissingDelimiterDiagnostic}}
> still relocates the caret for a missing closer (GROOVY-12169). Safe index is
> the same opener family as a normal index; if the innermost opener is not the
> family of the sole expected closer, defer so an unclosed index inside
> parentheses reports *Missing ']'*. Everything else only refines the fallback
> sentence and keeps ANTLR's offending token: reserved/misplaced keyword, then
> {{default:}} / {{default ->}} lookback (skip newlines; do not label an
> incomplete interface {{default}} method as outside switch), then a leading
> safe-index token without a receiver, then a singleton expected punctuation
> token ({{Missing '('}}, {{Missing ':'}}, {{Missing '>'}}, ...), then
> *Unexpected end of input* for EOF. Locate/refine stay in a defensive try;
> listener dispatch is a single call afterwards.
> {{AstBuilder}} reports the actual varargs parameter name.
> h3. Expected result (after)
> * unclosed single-, double-, or triple-quoted string becomes *Unclosed string
> literal*
> * unclosed block comment becomes *Unclosed comment* (caret on the opener)
> * zero-width space, no-break space, curly quote, em dash become {{Unexpected
> character: '\u200b'}} (and the matching escape)
> * {{if true}} / {{while true}} / {{for int i in ...}} without parentheses
> becomes {{Missing '('}}
> * {{x ? y}} becomes {{Missing ':'}}
> * a generic type missing {{>}} before {{(}} becomes {{Missing '>'}}
> * {{const x = 1}} becomes {{'const' is not supported; use 'val' or 'static
> final' instead}}
> * {{goto label}} becomes {{'goto' is not supported}}
> * {{threadsafe}} as a modifier becomes {{'threadsafe' is not supported}}
> * stray {{else}} / {{catch}} / {{case}} become {{'else' without 'if'}} /
> {{'catch' without 'try'}} / {{'case' outside of switch}}
> * {{default: x}} / {{default -> x}} at script scope becomes {{'default'
> outside of switch}}; an incomplete interface {{default}} method is not that
> message
> * unclosed or mismatched safe index, including inside parentheses, becomes
> *Missing ']'*
> * a safe index with no receiver, or {{?}} immediately before one, becomes:
> {noformat}
> '?[' requires an expression before it
> {noformat}
> * a space between the question mark and the opening bracket is an incomplete
> ternary ({{Missing ':'}}); safe index is one token:
> {noformat}
> a? [0]
> {noformat}
> * {{throw}} at EOF becomes *Unexpected end of input*
> * {{def n = 1_}} becomes *Number ending with underscores is invalid*
> (position once)
> * {{def m(int... a, int b)}} becomes {{The var-arg parameter a must be the
> last parameter}}
> Valid programs are unchanged.
> h3. Related
> GROOVY-12169, GROOVY-12171, GROOVY-10146
--
This message was sent by Atlassian Jira
(v8.20.10#820010)