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.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to