This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch GROOVY-12169
in repository https://gitbox.apache.org/repos/asf/groovy.git
The following commit(s) were added to refs/heads/GROOVY-12169 by this push:
new 1f18b8ac58 Minor tweak
1f18b8ac58 is described below
commit 1f18b8ac58c98e83493d984e44a4e1bcc337b498
Author: Daniel Sun <[email protected]>
AuthorDate: Sat Jul 18 11:47:05 2026 +0900
Minor tweak
---
.../internal/MissingDelimiterDiagnostic.java | 47 +++++++--
.../groovy/parser/antlr4/SyntaxErrorTest.groovy | 35 ++++++
.../internal/MissingDelimiterDiagnosticTest.groovy | 117 +++++++++++++++++++++
3 files changed, 193 insertions(+), 6 deletions(-)
diff --git
a/src/main/java/org/apache/groovy/parser/antlr4/internal/MissingDelimiterDiagnostic.java
b/src/main/java/org/apache/groovy/parser/antlr4/internal/MissingDelimiterDiagnostic.java
index d960f25e20..1da2c21fbd 100644
---
a/src/main/java/org/apache/groovy/parser/antlr4/internal/MissingDelimiterDiagnostic.java
+++
b/src/main/java/org/apache/groovy/parser/antlr4/internal/MissingDelimiterDiagnostic.java
@@ -18,12 +18,14 @@
*/
package org.apache.groovy.parser.antlr4.internal;
+import groovy.lang.Tuple2;
import org.antlr.v4.runtime.BufferedTokenStream;
import org.antlr.v4.runtime.CommonToken;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Token;
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.misc.IntervalSet;
+import org.apache.groovy.parser.antlr4.util.PositionConfigureUtils;
import java.util.ArrayDeque;
import java.util.ArrayList;
@@ -206,10 +208,16 @@ final class MissingDelimiterDiagnostic {
return depth;
}
+ /**
+ * Last non-EOF, non-NL token. Skipping NL keeps the caret on the last
+ * meaningful line (same idea as {@link #fromDelimiterStack}) instead of
+ * after a trailing newline that often sits alone on the next source line.
+ */
private static Token lastNonEof(final List<Token> tokens) {
for (int i = tokens.size() - 1; i >= 0; i--) {
Token t = tokens.get(i);
- if (t.getType() != Token.EOF) {
+ int type = t.getType();
+ if (type != Token.EOF && type != NL) {
return t;
}
}
@@ -231,6 +239,13 @@ final class MissingDelimiterDiagnostic {
* <li>the token after the type is a literal or {@code '('}
* (e.g. {@code (Foo)1}, {@code (Foo)(x)})</li>
* </ul>
+ * Reporting the earliest site is intentional: with
+ * {@link org.antlr.v4.runtime.BailErrorStrategy} the parser stops at the
+ * first recognition failure, so the first incomplete cast is the relevant
+ * one. In a hypothetical multi-error buffer the caret could theoretically
+ * point at an earlier cast-like fragment than the failure ANTLR reported;
+ * that trade-off is accepted to keep the scan linear and free of false
+ * coupling to ANTLR's sometimes-unrelated offending token.
*/
private static Hit fromCastPattern(final List<Token> tokens) {
final int n = tokens.size();
@@ -487,7 +502,9 @@ final class MissingDelimiterDiagnostic {
}
}
} catch (IndexOutOfBoundsException | IllegalArgumentException ignored)
{
- // Non-buffered stream: keep whatever was collected
+ // Non-buffered / partially filled streams may reject absolute
get(i)
+ // (e.g. UnbufferedTokenStream outside its window). Partial
default-
+ // channel tokens are still useful for best-effort diagnostics.
}
return result;
}
@@ -510,14 +527,32 @@ final class MissingDelimiterDiagnostic {
/**
* Caret sits just past {@code token} (the usual "insert closer here"
point).
+ * <p>
+ * Multi-line tokens (triple-quoted strings, multi-line GString fragments,
+ * …) place the caret on the <em>last</em> line of the token, after its
+ * final content. Adding the full code-point length to the start column
+ * alone would overshoot on the start line when the token text contains
+ * newlines.
+ * </p>
+ * <p>
+ * End line/column rules match {@link PositionConfigureUtils#endPosition}
+ * so diagnostic carets stay consistent with AST last-position handling
+ * (code-point based columns, newline counting).
+ * </p>
*/
private static Token insertionPointAfter(final Token token) {
- String text = token.getText();
- int length = text != null ? text.codePointCount(0, text.length()) : 0;
CommonToken point = new CommonToken(Token.INVALID_TYPE);
- point.setLine(token.getLine());
- point.setCharPositionInLine(token.getCharPositionInLine() + length);
point.setText("");
+ String text = token.getText();
+ if (text == null) {
+ point.setLine(token.getLine());
+ point.setCharPositionInLine(token.getCharPositionInLine());
+ return point;
+ }
+ // endPosition → (line, 1-based exclusive column); ANTLR columns are
0-based
+ Tuple2<Integer, Integer> end =
PositionConfigureUtils.endPosition(token);
+ point.setLine(end.getV1());
+ point.setCharPositionInLine(end.getV2() - 1);
return point;
}
}
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 ab1463934b..046ad00374 100644
--- a/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
+++ b/src/test/groovy/org/apache/groovy/parser/antlr4/SyntaxErrorTest.groovy
@@ -835,6 +835,41 @@ final class SyntaxErrorTest {
|'''.stripMargin()
}
+ @Test
+ void 'error alternative - Missing "}" after multi-line string'() {
+ // Caret must sit on the last line of the triple-quoted string, not on
+ // the start line with an overshot column (multi-line insertion point).
+ expectParseError '''\
+ |def m() {
+ | s = """
+ |line2
+ |line3"""
+ |'''.stripMargin(), '''\
+ |Missing '}' @ line 4, column 9.
+ | line3"""
+ | ^
+ |
+ |1 error
+ |'''.stripMargin()
+ }
+
+ @Test
+ void 'error alternative - Missing "}" after multi-line single-quoted
string'() {
+ // Outer double-quoted long strings avoid clashing with ''' inside the
source.
+ expectParseError("""\
+ |def m() {
+ | s = '''
+ |x
+ |y'''
+ |""".stripMargin(), """\
+ |Missing '}' @ line 4, column 5.
+ | y'''
+ | ^
+ |
+ |1 error
+ |""".stripMargin())
+ }
+
@NotYetImplemented @Test
void 'CompilerErrorTest_001'() {
unzipScriptAndShouldFail('scripts/CompilerErrorTest_001.groovy', [])
diff --git
a/src/test/groovy/org/apache/groovy/parser/antlr4/internal/MissingDelimiterDiagnosticTest.groovy
b/src/test/groovy/org/apache/groovy/parser/antlr4/internal/MissingDelimiterDiagnosticTest.groovy
index 7188e60f49..88dba5f9c1 100644
---
a/src/test/groovy/org/apache/groovy/parser/antlr4/internal/MissingDelimiterDiagnosticTest.groovy
+++
b/src/test/groovy/org/apache/groovy/parser/antlr4/internal/MissingDelimiterDiagnosticTest.groovy
@@ -43,9 +43,11 @@ import static
org.apache.groovy.parser.antlr4.GroovyParser.LBRACE
import static org.apache.groovy.parser.antlr4.GroovyParser.LBRACK
import static org.apache.groovy.parser.antlr4.GroovyParser.LPAREN
import static org.apache.groovy.parser.antlr4.GroovyParser.LT
+import static org.apache.groovy.parser.antlr4.GroovyParser.NL
import static org.apache.groovy.parser.antlr4.GroovyParser.RBRACE
import static org.apache.groovy.parser.antlr4.GroovyParser.RBRACK
import static org.apache.groovy.parser.antlr4.GroovyParser.RPAREN
+import static org.apache.groovy.parser.antlr4.GroovyParser.StringLiteral
import static org.junit.jupiter.api.Assertions.assertEquals
import static org.junit.jupiter.api.Assertions.assertNotNull
import static org.junit.jupiter.api.Assertions.assertNull
@@ -449,6 +451,70 @@ final class MissingDelimiterDiagnosticTest {
def hit =
MissingDelimiterDiagnostic.locate(tokenStream('["\uD83D\uDE00"'), null)
assertNotNull hit
assertEquals "Missing ']'", hit.message
+ // caret just past the string: '[' + '"' is col 0..1, string starts at
1; emoji is 1 code point + quotes
+ assertEquals 1, hit.at.line
+ assertTrue hit.at.charPositionInLine > 1
+ }
+
+ @Test
+ void 'insertion point after multi-line string does not overshoot start
line'() {
+ // paulk-asert review: multi-line anchor before missing '}' must put
caret on last line
+ def src = 'def m() {\n s = """\nline2\nline3"""\n'
+ def hit = MissingDelimiterDiagnostic.locate(tokenStream(src), null)
+ assertNotNull hit
+ assertEquals "Missing '}'", hit.message
+ assertEquals 4, hit.at.line
+ // "line3""" is 8 code points → exclusive end column 8 (0-based)
+ assertEquals 8, hit.at.charPositionInLine
+ }
+
+ @Test
+ void 'insertion point after multi-line GString-like triple quote single
quotes'() {
+ def src = "def m() {\n s = '''a\nb'''\n"
+ def hit = MissingDelimiterDiagnostic.locate(tokenStream(src), null)
+ assertNotNull hit
+ assertEquals "Missing '}'", hit.message
+ assertEquals 3, hit.at.line
+ // b''' → 4 code points, 0-based exclusive end = 4
+ assertEquals 4, hit.at.charPositionInLine
+ }
+
+ @Test
+ void 'insertionPointAfter multi-line matches PositionConfigureUtils end
line'() {
+ def t = tok(StringLiteral, '"""\nline2\nend"""')
+ t.line = 2
+ t.charPositionInLine = 6
+ def point = (Token) invoke('insertionPointAfter', t)
+ assertEquals 4, point.line
+ assertEquals 6, point.charPositionInLine // "end""" length 6
+ }
+
+ @Test
+ void 'lastNonEof skips trailing NL tokens'() {
+ def tokens = [
+ tok(Identifier, 'x'),
+ tok(NL, '\n'),
+ tok(Token.EOF, null)
+ ]
+ def last = (Token) invoke('lastNonEof', tokens)
+ assertEquals Identifier, last.type
+ assertEquals 'x', last.text
+ }
+
+ @Test
+ void 'sole expected EOF after multi-line string uses last line of
string'() {
+ def tokens = tokenStream('def m() {\n s = """\nline2\nline3"""\n')
+ def eof = lastToken(tokens)
+ def hit = MissingDelimiterDiagnostic.locate(tokens,
stubException(setOf(RBRACE), eof))
+ assertNotNull hit
+ assertEquals "Missing '}'", hit.message
+ assertEquals 4, hit.at.line
+ assertEquals 8, hit.at.charPositionInLine
+ }
+
+ @Test
+ void 'collectDefaultChannelTokens tolerates IllegalArgumentException'() {
+ assertNull MissingDelimiterDiagnostic.locate(new
IllegalArgTokenStream(), null)
}
//--------------------------------------------------------------------------
@@ -697,4 +763,55 @@ final class MissingDelimiterDiagnosticTest {
}
}
+ /**
+ * Token stream that throws {@link IllegalArgumentException} on {@code
get},
+ * covering the non-IndexOutOfBounds defensive catch branch.
+ */
+ private static final class IllegalArgTokenStream implements
org.antlr.v4.runtime.TokenStream {
+ @Override
+ Token LT(int k) { throw new IllegalArgumentException('test') }
+
+ @Override
+ Token get(int index) { throw new IllegalArgumentException('test') }
+
+ @Override
+ org.antlr.v4.runtime.TokenSource getTokenSource() { null }
+
+ @Override
+ String getText(org.antlr.v4.runtime.misc.Interval interval) { '' }
+
+ @Override
+ String getText() { '' }
+
+ @Override
+ String getText(org.antlr.v4.runtime.RuleContext ctx) { '' }
+
+ @Override
+ String getText(Object start, Object stop) { '' }
+
+ @Override
+ void consume() {}
+
+ @Override
+ int LA(int i) { Token.EOF }
+
+ @Override
+ int mark() { 0 }
+
+ @Override
+ void release(int marker) {}
+
+ @Override
+ int index() { 0 }
+
+ @Override
+ void seek(int index) {}
+
+ @Override
+ int size() { 0 }
+
+ @Override
+ String getSourceName() { 'illegal-arg' }
+ }
+
}