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 9045217216e252e6527f193a3ae8160e8395973a 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 ++++- .../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 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') } }
