This is an automated email from the ASF dual-hosted git repository.

asf-gitbox-commits pushed a commit to branch GROOVY-12171
in repository https://gitbox.apache.org/repos/asf/groovy.git

commit 4e6ac8fa3a5505ca5799e6cfd3747bb6465c9242
Author: Daniel Sun <[email protected]>
AuthorDate: Sat Jul 18 10:58:51 2026 +0900

    GROOVY-12171: Improve GString syntax error when '$' is not followed by a 
valid interpolation
---
 .../groovy/parser/antlr4/GroovyLangLexer.java      |  91 +++++++++++++++++-
 .../groovy/parser/antlr4/SyntaxErrorTest.groovy    |  65 +++++++++++++
 .../codehaus/groovy/antlr/GStringEndTest.groovy    | 106 ++++++++++++++++++---
 3 files changed, 245 insertions(+), 17 deletions(-)

diff --git a/src/main/java/org/apache/groovy/parser/antlr4/GroovyLangLexer.java 
b/src/main/java/org/apache/groovy/parser/antlr4/GroovyLangLexer.java
index 00af5045ef..888f84507e 100644
--- a/src/main/java/org/apache/groovy/parser/antlr4/GroovyLangLexer.java
+++ b/src/main/java/org/apache/groovy/parser/antlr4/GroovyLangLexer.java
@@ -22,6 +22,7 @@ import org.antlr.v4.runtime.CharStream;
 import org.antlr.v4.runtime.CharStreams;
 import org.antlr.v4.runtime.Lexer;
 import org.antlr.v4.runtime.LexerNoViableAltException;
+import org.antlr.v4.runtime.Token;
 import org.antlr.v4.runtime.atn.ATN;
 import org.antlr.v4.runtime.atn.LexerATNSimulator;
 import org.apache.groovy.parser.antlr4.internal.atnmanager.LexerAtnManager;
@@ -43,9 +44,67 @@ public class GroovyLangLexer extends GroovyLexer {
         this.setInterpreter(new PositionAdjustingLexerATNSimulator(this, 
LexerAtnManager.INSTANCE.getATN()));
     }
 
+    /**
+     * Returns the next token, with a special case for EOF after a bare {@code 
$} in a GString.
+     * <p>
+     * After {@code GStringBegin} or {@code GStringPart} matches a trailing 
{@code $} at the
+     * end of input, Antlr marks {@code _hitEOF} and would skip
+     * {@link #GSTRING_TYPE_SELECTOR_MODE} entirely on the subsequent call.  
Detect that
+     * situation and report the dollar-body error instead of a generic 
unexpected-EOF parse
+     * failure.
+     *
+     * @return the next token from the input
+     */
     @Override
-    public void recover(LexerNoViableAltException e) {
-        throw e; // if some lexical error occurred, stop parsing!
+    public Token nextToken() {
+        if (_hitEOF && isGStringDollarSelectorMode()) {
+            throw new GroovySyntaxError(
+                    illegalGStringDollarMessage(Token.EOF),
+                    GroovySyntaxError.LEXER,
+                    getLine(),
+                    getCharPositionInLine() + 1);
+        }
+        return super.nextToken();
+    }
+
+    /**
+     * Report a lexical error.  When the failure is inside
+     * {@link #GSTRING_TYPE_SELECTOR_MODE} (after a {@code $} in a GString), 
emit the
+     * dedicated dollar-body diagnostic instead of Antlr's generic 
token-recognition message.
+     *
+     * @param e the recognition failure raised by the ATN simulator
+     */
+    @Override
+    public void notifyListeners(final LexerNoViableAltException e) {
+        if (isGStringDollarSelectorMode()) {
+            getErrorListenerDispatch().syntaxError(
+                    this, null, _tokenStartLine, _tokenStartCharPositionInLine,
+                    illegalGStringDollarMessage(), e);
+            return;
+        }
+        super.notifyListeners(e);
+    }
+
+    /**
+     * Abort lexing on the first lexical error.  For an invalid character 
after {@code $}
+     * in a GString, throw {@link GroovySyntaxError} so the parser layer can 
surface a
+     * source-located syntax error without retrying SLL→LL or falling back to 
a generic
+     * exception message.
+     *
+     * @param e the recognition failure raised by the ATN simulator
+     */
+    @Override
+    public void recover(final LexerNoViableAltException e) {
+        if (isGStringDollarSelectorMode()) {
+            // Column is 1-based; _tokenStartCharPositionInLine is 0-based 
(Antlr convention).
+            throw new GroovySyntaxError(
+                    illegalGStringDollarMessage(),
+                    GroovySyntaxError.LEXER,
+                    _tokenStartLine,
+                    _tokenStartCharPositionInLine + 1);
+        }
+        // if some lexical error occurred, stop parsing!
+        throw e;
     }
 
     @Override
@@ -53,6 +112,34 @@ public class GroovyLangLexer extends GroovyLexer {
         ((PositionAdjustingLexerATNSimulator) 
getInterpreter()).resetAcceptPosition(getInputStream(), _tokenStartCharIndex - 
1, _tokenStartLine, _tokenStartCharPositionInLine - 1);
     }
 
+    /**
+     * Whether the lexer is choosing between {@code ${...}} and {@code 
$identifier} after a
+     * dollar in a GString (double-quoted, triple-double-quoted, slashy or 
dollar-slashy).
+     */
+    private boolean isGStringDollarSelectorMode() {
+        return GSTRING_TYPE_SELECTOR_MODE == _mode;
+    }
+
+    /**
+     * Build the user-facing message for an illegal character (or EOF) 
immediately after
+     * {@code $} in a GString.  When a concrete character is available it is 
appended so the
+     * diagnostic names both the rule and the offending input.
+     */
+    private String illegalGStringDollarMessage() {
+        return illegalGStringDollarMessage(_input.LA(1));
+    }
+
+    /**
+     * @param c the code point at the failure site, or {@link Token#EOF}
+     */
+    private String illegalGStringDollarMessage(final int c) {
+        // Antlr leaves the input index at the unrecognised character for 
LexerNoViableAltException.
+        if (Token.EOF == c) {
+            return "Illegal string body character after dollar sign";
+        }
+        return "Illegal string body character after dollar sign: " + 
getCharErrorDisplay(c);
+    }
+
     private static class PositionAdjustingLexerATNSimulator extends 
LexerATNSimulator {
         public PositionAdjustingLexerATNSimulator(Lexer recog, ATN atn) {
             super(recog, atn);
diff --git 
a/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy 
b/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
index bbe0175f72..9593d78133 100644
--- a/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
+++ b/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
@@ -131,6 +131,71 @@ final class SyntaxErrorTest {
             |'''.stripMargin()
     }
 
+    @Test
+    void 'groovy core - GString illegal character after dollar'() {
+        // Trailing bare `$` in double-quoted and triple-double-quoted strings.
+        // Must name the dollar (Groovy 2 message) rather than only the 
closing quote.
+        expectParseError '''\
+            |def Target = "releases$"
+            |'''.stripMargin(), '''\
+            |Illegal string body character after dollar sign: '"' @ line 1, 
column 24.
+            |   def Target = "releases$"
+            |                          ^
+            |
+            |1 error
+            |'''.stripMargin()
+
+        expectParseError '''\
+            |def Target = """releases$"""
+            |'''.stripMargin(), '''\
+            |Illegal string body character after dollar sign: '"' @ line 1, 
column 26.
+            |   def Target = """releases$"""
+            |                            ^
+            |
+            |1 error
+            |'''.stripMargin()
+
+        expectParseError '''\
+            |def x = "$"
+            |'''.stripMargin(), '''\
+            |Illegal string body character after dollar sign: '"' @ line 1, 
column 11.
+            |   def x = "$"
+            |             ^
+            |
+            |1 error
+            |'''.stripMargin()
+
+        expectParseError '''\
+            |def x = "a$ b"
+            |'''.stripMargin(), '''\
+            |Illegal string body character after dollar sign: ' ' @ line 1, 
column 12.
+            |   def x = "a$ b"
+            |              ^
+            |
+            |1 error
+            |'''.stripMargin()
+
+        // newline immediately after `$` (unclosed GString line)
+        expectParseError '''\
+            |def x = "hello$
+            |'''.stripMargin(), '''\
+            |Illegal string body character after dollar sign: '\\n' @ line 1, 
column 16.
+            |   def x = "hello$
+            |                  ^
+            |
+            |1 error
+            |'''.stripMargin()
+
+        // true EOF immediately after `$` — no character to display in the 
message
+        expectParseError 'def x = "hello$', '''\
+            |Illegal string body character after dollar sign @ line 1, column 
16.
+            |   def x = "hello$
+            |                  ^
+            |
+            |1 error
+            |'''.stripMargin()
+    }
+
     @Test
     void 'groovy core - ParExpression'() {
         TestUtils.doRunAndShouldFail('fail/ParExpression_01x.groovy')
diff --git a/src/test/groovy/org/codehaus/groovy/antlr/GStringEndTest.groovy 
b/src/test/groovy/org/codehaus/groovy/antlr/GStringEndTest.groovy
index d9062f5293..70833a4b94 100644
--- a/src/test/groovy/org/codehaus/groovy/antlr/GStringEndTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/antlr/GStringEndTest.groovy
@@ -22,31 +22,107 @@ import 
org.codehaus.groovy.control.MultipleCompilationErrorsException
 import org.junit.jupiter.api.Test
 
 import static groovy.test.GroovyAssert.assertScript
+import static groovy.test.GroovyAssert.shouldFail
 
+/**
+ * Error diagnostics for GStrings that end with a bare {@code $}
+ * (or otherwise place an illegal character immediately after {@code $}).
+ * <p>
+ * Historical Groovy 2 reported
+ * {@code illegal string body character after dollar sign} with a precise
+ * caret.  The Antlr4 lexer must preserve that clarity rather than emitting
+ * the generic {@code token recognition error at: '"'} that points at the
+ * closing quote without mentioning the dollar.
+ */
 class GStringEndTest {
+
+    private static final String ILLEGAL_AFTER_DOLLAR = 'Illegal string body 
character after dollar sign'
+
     @Test
-    void testInvalidEndContainsLineNumber(){
-        try {
-            assertScript '''
+    void testInvalidEndContainsLineNumber() {
+        def err = shouldFail(MultipleCompilationErrorsException,
+             '''
                 def Target = "releases$"
-            '''
-        } catch (MultipleCompilationErrorsException mcee) {
-            def text = mcee.toString();
-            assert text.contains("line 2, column 40")
-        }
+            ''')
+        def text = err.toString()
+        assert text.contains('line 2, column 40')
+        assert text.contains(ILLEGAL_AFTER_DOLLAR)
+        // caret sample still points at the illegal character (the closing 
quote after $)
+        assert text.contains('def Target = "releases$"')
+        assert !text.contains('token recognition error')
+    }
+
+    @Test
+    void testInvalidEndInTripleDoubleQuotedString() {
+        def err = shouldFail(MultipleCompilationErrorsException,
+             '''
+                def Target = """releases$"""
+            ''')
+        def text = err.toString()
+        assert text.contains(ILLEGAL_AFTER_DOLLAR)
+        assert text.contains('line 2, column 42')
+        assert !text.contains('token recognition error')
+    }
+
+    @Test
+    void testDollarOnlyGString() {
+        def err = shouldFail(MultipleCompilationErrorsException,
+             'def x = "$"')
+        def text = err.toString()
+        assert text.contains(ILLEGAL_AFTER_DOLLAR)
+        assert text.contains('line 1, column 11')
+        assert !text.contains('token recognition error')
+    }
+
+    @Test
+    void testIllegalCharacterAfterDollarNotClosingQuote() {
+        // space after $ is also illegal (not an identifier / not ${)
+        def err = shouldFail(MultipleCompilationErrorsException,
+             'def x = "a$ b"')
+        def text = err.toString()
+        assert text.contains(ILLEGAL_AFTER_DOLLAR)
+        assert text.contains('line 1, column 12')
+        assert !text.contains('token recognition error')
+    }
+
+    @Test
+    void testMidStringDollarPartWithTrailingDollar() {
+        def err = shouldFail(MultipleCompilationErrorsException,
+             'def x = "hello $name$"')
+        def text = err.toString()
+        assert text.contains(ILLEGAL_AFTER_DOLLAR)
+        assert !text.contains('token recognition error')
+    }
+
+    @Test
+    void testEscapedTrailingDollarIsLegal() {
+        assertScript '''
+            def Target = "releases\\$"
+            assert Target == 'releases$'
+        '''
+    }
+
+    @Test
+    void testDollarAtEndOfFile() {
+        // GStringBegin consumes up through `$`, then EOF — no character to 
quote in the message
+        def err = shouldFail(MultipleCompilationErrorsException,
+             'def x = "hello$')
+        def text = err.toString()
+        assert text.contains(ILLEGAL_AFTER_DOLLAR)
+        assert !text.contains('token recognition error')
+        // EOF form has no trailing ": '…'" character display
+        assert !(text =~ /Illegal string body character after dollar sign: '/)
     }
 
     @Test
     void testErrorReportOnStringEndWithOutParser() {
         // GROOVY-6608: the code did throw a NPE
-        try {
-            assertScript '''
+        def err = shouldFail(MultipleCompilationErrorsException,
+             '''
             def scanFolders()
             { doThis( ~"(?i)^sometext$",
-            '''
-        } catch (MultipleCompilationErrorsException mcee) {
-            def text = mcee.toString();
-            assert text.contains("line 3, column 39")
-        }
+            ''')
+        def text = err.toString()
+        assert text.contains('line 3, column 39')
     }
 }

Reply via email to