This is an automated email from the ASF dual-hosted git repository.
daniellansun pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/master by this push:
new c76e8dc8df GROOVY-12171: Improve GString syntax error when '$' is not
followed by a valid interpolation (#2719)
c76e8dc8df is described below
commit c76e8dc8df30e569789792a250280a447b25bb69
Author: Daniel Sun <[email protected]>
AuthorDate: Sat Jul 18 14:39:36 2026 +0800
GROOVY-12171: Improve GString syntax error when '$' is not followed by a
valid interpolation (#2719)
---
.../groovy/parser/antlr4/GroovyLangLexer.java | 91 ++++-
.../antlr4/GroovyLangLexerGStringDollarTest.groovy | 377 +++++++++++++++++++++
.../groovy/parser/antlr4/SyntaxErrorTest.groovy | 65 ++++
.../codehaus/groovy/antlr/GStringEndTest.groovy | 106 +++++-
4 files changed, 622 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/GroovyLangLexerGStringDollarTest.groovy
b/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyLangLexerGStringDollarTest.groovy
new file mode 100644
index 0000000000..90e5153d9c
--- /dev/null
+++
b/src/test/groovy/org/apache/groovy/parser/antlr4/GroovyLangLexerGStringDollarTest.groovy
@@ -0,0 +1,377 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.groovy.parser.antlr4
+
+import org.antlr.v4.runtime.BaseErrorListener
+import org.antlr.v4.runtime.CharStreams
+import org.antlr.v4.runtime.LexerNoViableAltException
+import org.antlr.v4.runtime.RecognitionException
+import org.antlr.v4.runtime.Recognizer
+import org.antlr.v4.runtime.Token
+import org.junit.jupiter.api.Test
+
+import static groovy.test.GroovyAssert.shouldFail
+import static
org.apache.groovy.parser.antlr4.GroovyLexer.GSTRING_TYPE_SELECTOR_MODE
+import static org.junit.jupiter.api.Assertions.assertEquals
+import static org.junit.jupiter.api.Assertions.assertFalse
+import static org.junit.jupiter.api.Assertions.assertSame
+import static org.junit.jupiter.api.Assertions.assertTrue
+
+/**
+ * Unit-level coverage for {@link GroovyLangLexer}'s GString dollar-body
diagnostics
+ * (GROOVY-12171).
+ * <p>
+ * Exercises every branch of {@code nextToken}, {@code notifyListeners},
{@code recover}
+ * and the private dollar-message helpers: both the GString-selector mode and
the
+ * generic non-GString fall-through, plus the EOF vs. concrete-character
message forms.
+ */
+final class GroovyLangLexerGStringDollarTest {
+
+ private static final String ILLEGAL_AFTER_DOLLAR = 'Illegal string body
character after dollar sign'
+
+ //
-------------------------------------------------------------------------
+ // constructors
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void 'Reader constructor tokenizes the same as CharStream constructor'() {
+ def source = 'def x = 1'
+ def fromReader = new GroovyLangLexer(new StringReader(source))
+ def fromStream = lexer(source)
+ assertEquals fromStream.nextToken().type, fromReader.nextToken().type
+ }
+
+ //
-------------------------------------------------------------------------
+ // nextToken — success path (super.nextToken)
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void 'nextToken returns ordinary tokens outside GString dollar mode'() {
+ def lexer = lexer('def x = 1')
+ def types = []
+ Token t
+ while ((t = lexer.nextToken()).type != Token.EOF) {
+ if (t.channel == Token.DEFAULT_CHANNEL) {
+ types << t.type
+ }
+ }
+ assertFalse types.isEmpty()
+ assertEquals Token.EOF, t.type
+ }
+
+ //
-------------------------------------------------------------------------
+ // nextToken — _hitEOF + GSTRING_TYPE_SELECTOR_MODE (EOF after bare $)
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void 'nextToken reports dollar-body error when EOF follows bare dollar in
GString'() {
+ // GStringBegin consumes up through `$`, sets _hitEOF because
LA(1)==EOF,
+ // and leaves the lexer in GSTRING_TYPE_SELECTOR_MODE. The subsequent
+ // nextToken must not silently emit EOF.
+ def lexer = lexer('"hello$')
+ Token first = lexer.nextToken()
+ assertEquals GroovyLexer.GStringBegin, first.type
+
+ def err = shouldFail(GroovySyntaxError) {
+ lexer.nextToken()
+ }
+ assertEquals ILLEGAL_AFTER_DOLLAR, err.message
+ assertEquals GroovySyntaxError.LEXER, err.source
+ assertTrue err.line >= 1
+ assertTrue err.column >= 1
+ }
+
+ @Test
+ void 'nextToken EOF dollar error for dollar-only open GString'() {
+ def lexer = lexer('"$')
+ assertEquals GroovyLexer.GStringBegin, lexer.nextToken().type
+
+ def err = shouldFail(GroovySyntaxError) {
+ lexer.nextToken()
+ }
+ assertEquals ILLEGAL_AFTER_DOLLAR, err.message
+ // EOF form has no ": '…'" character display
+ assertFalse err.message.contains(':')
+ }
+
+ //
-------------------------------------------------------------------------
+ // nextToken / recover / notifyListeners — illegal character after $
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void 'lex of closing quote after dollar throws GroovySyntaxError with
quoted char'() {
+ // notifyListeners (GString branch) + recover (GString branch) + char
message
+ def err = shouldFail(GroovySyntaxError) {
+ drain(lexer('"releases$"'))
+ }
+ assertTrue err.message.startsWith(ILLEGAL_AFTER_DOLLAR + ':')
+ assertTrue err.message.contains('"') || err.message.contains('\\"')
+ assertEquals GroovySyntaxError.LEXER, err.source
+ }
+
+ @Test
+ void 'lex of space after dollar throws GroovySyntaxError naming the
space'() {
+ def err = shouldFail(GroovySyntaxError) {
+ drain(lexer('"a$ b"'))
+ }
+ assertTrue err.message.contains(ILLEGAL_AFTER_DOLLAR)
+ assertTrue err.message.contains(' ')
+ }
+
+ @Test
+ void 'lex of newline after dollar throws GroovySyntaxError naming the
newline'() {
+ def err = shouldFail(GroovySyntaxError) {
+ drain(lexer('"hello$\n'))
+ }
+ assertTrue err.message.contains(ILLEGAL_AFTER_DOLLAR)
+ assertTrue err.message.contains('\\n') || err.message.contains('\n')
+ }
+
+ @Test
+ void 'lex of digit after dollar is illegal'() {
+ // IdentifierInGString cannot start with a digit
+ def err = shouldFail(GroovySyntaxError) {
+ drain(lexer('"val$1"'))
+ }
+ assertTrue err.message.contains(ILLEGAL_AFTER_DOLLAR)
+ assertTrue err.message.contains('1')
+ }
+
+ @Test
+ void 'lex of punctuation after dollar is illegal'() {
+ def err = shouldFail(GroovySyntaxError) {
+ drain(lexer('"val$,rest"'))
+ }
+ assertTrue err.message.contains(ILLEGAL_AFTER_DOLLAR)
+ assertTrue err.message.contains(',')
+ }
+
+ @Test
+ void 'triple-double-quoted GString trailing dollar reports dollar-body
error'() {
+ def err = shouldFail(GroovySyntaxError) {
+ drain(lexer('"""releases$"""'))
+ }
+ assertTrue err.message.contains(ILLEGAL_AFTER_DOLLAR)
+ }
+
+ @Test
+ void 'GStringPart mid-string trailing dollar reports dollar-body error'() {
+ // After $name the next $ pushes GSTRING_TYPE_SELECTOR_MODE again;
closing "
+ // is then illegal
+ def err = shouldFail(GroovySyntaxError) {
+ drain(lexer('"hello $name$"'))
+ }
+ assertTrue err.message.contains(ILLEGAL_AFTER_DOLLAR)
+ }
+
+ @Test
+ void 'valid GString interpolation tokenizes without dollar-body error'() {
+ def types = []
+ def lexer = lexer('"hello $name!"')
+ Token t
+ while ((t = lexer.nextToken()).type != Token.EOF) {
+ if (t.channel == Token.DEFAULT_CHANNEL) {
+ types << t.type
+ }
+ }
+ assertTrue types.contains(GroovyLexer.GStringBegin)
+ assertTrue types.contains(GroovyLexer.Identifier)
+ assertTrue types.contains(GroovyLexer.GStringEnd)
+ }
+
+ @Test
+ void 'valid brace interpolation tokenizes without dollar-body error'() {
+ def lexer = lexer('"${1+2}"')
+ def sawLBrace = false
+ Token t
+ while ((t = lexer.nextToken()).type != Token.EOF) {
+ if (t.type == GroovyLexer.LBRACE) {
+ sawLBrace = true
+ }
+ }
+ assertTrue sawLBrace
+ }
+
+ //
-------------------------------------------------------------------------
+ // recover — non-GString fall-through rethrows LexerNoViableAltException
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void 'recover outside GString dollar mode rethrows the recognition
failure'() {
+ def lexer = lexer('x')
+ def e = new LexerNoViableAltException(lexer, lexer.inputStream, 0,
null)
+ def thrown = shouldFail(LexerNoViableAltException) {
+ lexer.recover(e)
+ }
+ assertSame e, thrown
+ }
+
+ //
-------------------------------------------------------------------------
+ // recover — GString dollar mode → GroovySyntaxError (char + EOF messages)
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void 'recover in GString dollar mode with concrete char throws
GroovySyntaxError'() {
+ def lexer = lexer('!')
+ lexer.pushMode(GSTRING_TYPE_SELECTOR_MODE)
+ def e = new LexerNoViableAltException(lexer, lexer.inputStream, 0,
null)
+
+ def err = shouldFail(GroovySyntaxError) {
+ lexer.recover(e)
+ }
+ assertTrue err.message.startsWith(ILLEGAL_AFTER_DOLLAR + ':')
+ assertEquals GroovySyntaxError.LEXER, err.source
+ }
+
+ @Test
+ void 'recover in GString dollar mode at EOF throws GroovySyntaxError
without char display'() {
+ def lexer = lexer('')
+ lexer.pushMode(GSTRING_TYPE_SELECTOR_MODE)
+ def e = new LexerNoViableAltException(lexer, lexer.inputStream, 0,
null)
+
+ def err = shouldFail(GroovySyntaxError) {
+ lexer.recover(e)
+ }
+ assertEquals ILLEGAL_AFTER_DOLLAR, err.message
+ assertFalse err.message.contains(':')
+ assertEquals GroovySyntaxError.LEXER, err.source
+ }
+
+ //
-------------------------------------------------------------------------
+ // notifyListeners — GString dollar mode emits dedicated diagnostic
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void 'notifyListeners in GString dollar mode reports illegal dollar-body
message'() {
+ def lexer = lexer('!')
+ lexer.pushMode(GSTRING_TYPE_SELECTOR_MODE)
+ def messages = []
+ lexer.removeErrorListeners()
+ lexer.addErrorListener(new BaseErrorListener() {
+ @Override
+ void syntaxError(Recognizer<?, ?> recognizer, Object
offendingSymbol,
+ int line, int charPositionInLine, String msg,
+ RecognitionException e) {
+ messages << msg
+ }
+ })
+ def e = new LexerNoViableAltException(lexer, lexer.inputStream, 0,
null)
+ lexer.notifyListeners(e)
+
+ assertEquals 1, messages.size()
+ assertTrue messages[0].startsWith(ILLEGAL_AFTER_DOLLAR + ':')
+ }
+
+ @Test
+ void 'notifyListeners in GString dollar mode at EOF reports message
without char'() {
+ def lexer = lexer('')
+ lexer.pushMode(GSTRING_TYPE_SELECTOR_MODE)
+ def messages = []
+ lexer.removeErrorListeners()
+ lexer.addErrorListener(new BaseErrorListener() {
+ @Override
+ void syntaxError(Recognizer<?, ?> recognizer, Object
offendingSymbol,
+ int line, int charPositionInLine, String msg,
+ RecognitionException e) {
+ messages << msg
+ }
+ })
+ def e = new LexerNoViableAltException(lexer, lexer.inputStream, 0,
null)
+ lexer.notifyListeners(e)
+
+ assertEquals 1, messages.size()
+ assertEquals ILLEGAL_AFTER_DOLLAR, messages[0]
+ }
+
+ //
-------------------------------------------------------------------------
+ // notifyListeners — non-GString fall-through (generic Antlr message)
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void 'notifyListeners outside GString dollar mode uses generic recognition
message'() {
+ def lexer = lexer('!')
+ // super.notifyListeners reads [_tokenStartCharIndex, index]; seed a
valid span
+ // (same package → protected Lexer fields are accessible).
+ lexer._tokenStartCharIndex = 0
+ def messages = []
+ lexer.removeErrorListeners()
+ lexer.addErrorListener(new BaseErrorListener() {
+ @Override
+ void syntaxError(Recognizer<?, ?> recognizer, Object
offendingSymbol,
+ int line, int charPositionInLine, String msg,
+ RecognitionException e) {
+ messages << msg
+ }
+ })
+ def e = new LexerNoViableAltException(lexer, lexer.inputStream, 0,
null)
+ lexer.notifyListeners(e)
+
+ assertEquals 1, messages.size()
+ assertTrue messages[0].startsWith('token recognition error'),
+ "expected generic Antlr message, got: ${messages[0]}"
+ assertFalse messages[0].contains(ILLEGAL_AFTER_DOLLAR)
+ }
+
+ //
-------------------------------------------------------------------------
+ // Integration-style: forced dollar-selector mode (match path, not just
+ // the nextToken _hitEOF short-circuit after GStringBegin)
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void 'EOF while already in GString dollar mode yields dollar-body error on
subsequent nextToken'() {
+ // Antlr's ATN returns Token.EOF without throwing when the input is
already
+ // exhausted and nothing was consumed (failOrAccept). That sets
_hitEOF;
+ // the next call then hits GroovyLangLexer.nextToken's dollar-mode
guard.
+ def lexer = lexer('')
+ lexer.pushMode(GSTRING_TYPE_SELECTOR_MODE)
+ assertEquals Token.EOF, lexer.nextToken().type
+
+ def err = shouldFail(GroovySyntaxError) {
+ lexer.nextToken()
+ }
+ assertEquals ILLEGAL_AFTER_DOLLAR, err.message
+ assertEquals GroovySyntaxError.LEXER, err.source
+ }
+
+ @Test
+ void 'match failure on illegal char in GString dollar mode yields
dollar-body error'() {
+ def lexer = lexer(' ')
+ lexer.pushMode(GSTRING_TYPE_SELECTOR_MODE)
+ def err = shouldFail(GroovySyntaxError) {
+ lexer.nextToken()
+ }
+ assertTrue err.message.startsWith(ILLEGAL_AFTER_DOLLAR + ':')
+ }
+
+ //
-------------------------------------------------------------------------
+ // helpers
+ //
-------------------------------------------------------------------------
+
+ private static GroovyLangLexer lexer(String source) {
+ new GroovyLangLexer(CharStreams.fromString(source))
+ }
+
+ /** Consume tokens until EOF or an exception escapes. */
+ private static void drain(GroovyLangLexer lexer) {
+ Token t
+ while ((t = lexer.nextToken()).type != Token.EOF) {
+ // keep draining
+ }
+ }
+}
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 046ad00374..7f1a591f1d 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')
}
}