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' }
+    }
+
 }

Reply via email to