[
https://issues.apache.org/jira/browse/GROOVY-12694?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18138384#comment-18138384
]
ASF GitHub Bot commented on GROOVY-12694:
-----------------------------------------
codeconsole opened a new pull request, #3215:
URL: https://github.com/apache/groovy/pull/3215
Fixes GROOVY-12694.
What changed
The "unclosed cast" heuristic in MissingDelimiterDiagnostic (strategy 2,
fromCastPattern) now ignores a `(` whose matching `)` comes before the point
where the parser failed. That point is the offending token, or, for a
NoViableAltException, the start token of the failed decision when that is
earlier. When the exception carries no position (null exception or unindexed
tokens) the scan is unchanged. The bookkeeping is one linear pass over the
default-channel tokens up to the failure point with a stack of open parens, so
the error path stays linear.
Why
The scan looked at the whole token stream for `( type <literal | identifier
| (>` and reported the earliest match as a cast missing its `)`, without asking
whether that group had already been closed. Groups the parser had long accepted
match that shape: a record or method header with a primitive-typed parameter
(`record Circle(double radius)`, `def foo(int x)`), a classic `for (int i = 0;
...)` header, or a parenthesised call such as `(Math.max(1, 2))`. Any syntax
error later in the file was then reported as `Missing ')'` on that earlier
line. The reported case is an unsupported type pattern in a switch case label
(`case Circle c ->`) after a record declaration: `Missing ')' @ line 1, column
22` inside the well-formed record header instead of `Unexpected input: 'String
s = switch (o) { case Circle c' @ line 3, column 37`, which is what the same
switch gets after a plain class.
Real unclosed casts keep their diagnostics because a group that still
contains the failure point, or has no closer at all, is still scanned: `println
((int 123)` keeps `Missing ')'` at `123`, and the existing `for (int i=0; ...)`
call-argument case in ParserNegativeSyntaxTest (where the offending token is
the `(` of the group itself) keeps its caret.
Testing
- ParserNegativeSyntaxTest: two new end-to-end cases (record declaration
followed by `case Circle c ->`; a for header, a primitive-typed parameter and a
parenthesised call followed by a later syntax error).
- MissingDelimiterDiagnosticTest: cases for a group closed before the
failure (with and without another unclosed delimiter), a group holding the
offending token, the NoViableAltException start token, and a failure without a
position. The existing reflection tests for fromCastPattern pass the new second
argument.
- The new end-to-end cases and the two "closed before the failure" unit
cases fail on master without the main change and pass with it.
- `./gradlew :test --tests
org.apache.groovy.parser.antlr4.internal.MissingDelimiterDiagnosticTest --tests
org.apache.groovy.parser.antlr4.ParserNegativeSyntaxTest` on JDK 21: 82 + 235
tests, 0 failures. Without the main change the same run fails the four
behavioural cases named above (plus the reflection tests, which then see the
old one-argument signature).
- `./gradlew :test --tests 'org.apache.groovy.parser.antlr4.*'`: 14 classes,
828 tests, 0 failures, 1 skipped.
- `./gradlew :checkstyleMain`: no new findings in
MissingDelimiterDiagnostic.java; the only entry for the file is the
pre-existing cyclomatic-complexity warning on fromCastPattern (15, unchanged
from master because the candidate filtering lives in the new helper).
- The installed `groovyc --check` from this branch reports the reproducer at
`3:37: Unexpected input: 'String s = switch (o) { case Circle c'`; 6.0.0,
6.0.1-SNAPSHOT and master before the change report `1:22: Missing ')'`.
Backport
The affected code is identical on GROOVY_6_0_X; the change applies cleanly
there and is suggested for 6.0.1.
> Parser reports Missing ')' at an earlier closed paren group instead of the
> actual syntax error
> ----------------------------------------------------------------------------------------------
>
> Key: GROOVY-12694
> URL: https://issues.apache.org/jira/browse/GROOVY-12694
> Project: Groovy
> Issue Type: Bug
> Components: parser
> Affects Versions: 6.0.0
> Reporter: Scott Murphy Heiberg
> Priority: Major
>
> *Environment:* macOS 26.5 (Darwin 25.5.0), JDK 21.0.12 (BellSoft Liberica);
> diagnostics taken from {{groovyc --check --error-format=short}}. Also
> reproduces on 6.0.1-SNAPSHOT (GROOVY_6_0_X at 7c0dcf17dd) and master
> 7.0.0-SNAPSHOT (d86ea7eb85).
> h3. Description
> A Java-style type pattern in a switch case label ({{case Circle c ->}}) is
> not Groovy syntax, so the script below must fail to parse. When a record
> declaration precedes the switch, however, the error is reported as {{Missing
> ')'}} inside the record header on line 1 -- which parses fine on its own --
> instead of at the offending case label on line 3.
> h3. Reproducer
> {code:groovy}
> record Circle(double radius) {}
> Object o = new Circle(1)
> String s = switch (o) { case Circle c -> 'circle'; default -> 'other' }
> println s
> {code}
> h3. Actual
> {noformat}
> record.groovy:1:22: error: Missing ')'
> {noformat}
> Column 22 is {{radius}} in the record header. Replacing {{case Circle c ->}}
> with {{case Circle ->}} makes the file compile, so there is nothing missing
> on line 1.
> h3. Expected
> The diagnostic at the unsupported case label, which is what the same switch
> gets when a plain class ({{class Circle { double radius }}}) precedes it
> instead of the record:
> {noformat}
> record.groovy:3:37: error: Unexpected input: 'String s = switch (o) { case
> Circle c'
> {noformat}
> h3. Scope
> The record is incidental. Any parenthesised group of the shape {{( <type>
> <literal | identifier | (>}} that the parser has already accepted is reported
> as a missing {{)}} whenever a syntax error occurs later in the file:
> {code:groovy}
> def foo(int x) { x }
> def y = 1 +
> {code}
> {noformat}
> Missing ')' @ line 1, column 13
> {noformat}
> Expected {{Unexpected input: '+' @ line 2, column 11}}, which is what the
> second line reports on its own. The same happens with {{for (int i = 0; i <
> 3; i++) { }}} or {{def y = (Math.max(1, 2))}} on the first line.
> h3. Cause
> {{org.apache.groovy.parser.antlr4.internal.MissingDelimiterDiagnostic#fromCastPattern}}
> (the "unclosed cast" heuristic added for the friendlier missing-delimiter
> messages) scans the whole token stream for {{'(' type <expr-start>}} and
> reports the earliest match, without checking whether that group's {{)}} was
> already matched before the point where the parser failed. A formal parameter
> list with a primitive-typed parameter, a classic {{for}} header or a
> parenthesised call all match that pattern. Restricting the scan to groups
> that are still open at the failure point (the offending token, or the start
> token of a failed {{NoViableAltException}} decision) fixes the position while
> keeping the real unclosed-cast diagnostics ({{println ((int 123)}} still
> reports at {{123}}).
> A pull request with regression tests follows.
--
This message was sent by Atlassian Jira
(v10.3.26#10030026)