[
https://issues.apache.org/jira/browse/GROOVY-12353?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Daniel Sun updated GROOVY-12353:
--------------------------------
Description:
h3. Problem
After GROOVY-12169 and GROOVY-12171, several everyday syntax mistakes still
produce a generic *Unexpected input* or *Unexpected character* message that
does not name the actual problem.
Invisible or look-alike characters (zero-width space, BOM, NUL, no-break space,
curly quotes, non-ASCII dashes) render as an empty or misleading glyph between
quotes. Unclosed quotes and comments are reported as an unexpected quote or
slash rather than as an unclosed literal. {{if true}} without parentheses, {{x
? y}}, and {{const x = 1}} look like random unexpected tokens instead of a
missing {{(}} / {{:}} or an unimplemented keyword. Groovy 4 safe index is a
single token that pairs with a closing bracket, but an unclosed index inside
parentheses was reported as {{Missing ')'}}.
h3. Examples (before)
* Unclosed string:
{noformat}
println 'Hello
{noformat}
reports:
{noformat}
Unexpected character: ''' @ line 1, column 9.
{noformat}
(caret on the opening quote)
* Unclosed block comment:
{noformat}
/* comment
{noformat}
reports:
{noformat}
Unexpected input: '/'
{noformat}
* Invisible character ({{def}} + U+200B + {{name = null}}):
{noformat}
Unexpected character: ''
{noformat}
* Missing punctuation:
{noformat}
if true { x = 1 }
{noformat}
reports:
{noformat}
Unexpected input: 'true'
{noformat}
* Reserved keyword:
{noformat}
const x = 1
{noformat}
reports:
{noformat}
Unexpected input: 'const'
{noformat}
* Duplicate location:
{noformat}
def n = 1_
{noformat}
reports:
{noformat}
Number ending with underscores is invalid @ line 1, column 10 @ line 1, column
10.
{noformat}
* Hard-coded varargs name:
{noformat}
def m(int... a, int b) {}
{noformat}
reports:
{noformat}
The var-arg parameter strs must be the last parameter
{noformat}
* Unclosed safe index inside parentheses:
{noformat}
(a?[0
{noformat}
reports:
{noformat}
Missing ')'
{noformat}
h3. Root cause
* Lexer {{UNEXPECTED_CHAR}} inlined the raw character with only a quote-escape,
so control, format, and look-alike characters vanish or mislead. An unexpected
quote is almost always an unclosed string, but the message never said so.
* Unclosed block comments failed the comment rule and were retokenized as
{{/}}, so the parser saw an unexpected slash.
* Parser fallback wording was still ANTLR's *Unexpected input*, even when the
expected set was a single punctuation token, or the offender was a reserved or
misplaced keyword ({{const}}, {{goto}}, {{threadsafe}}, {{else}}, {{catch}},
{{finally}}, {{case}}) or EOF. For {{default:}} / {{default ->}} outside a
switch, the offender is {{:}} or {{->}}, not {{default}}.
* The missing-closer diagnostic trusted a sole expected closer. For an unclosed
safe index inside parentheses the parser expected {{)}}, so the inner unclosed
index was reported as {{Missing ')'}}. The safe-index token was not treated as
the same opener family as a normal index.
* Lexer {{require(..., true)}} appended {{@ line N, column M}} to
{{GroovySyntaxError}}, and {{SyntaxException}} appended the same location again.
* {{AstBuilder}} used a hard-coded parameter name {{strs}} in the
varargs-not-last diagnostic.
Grammar-level parser error alternatives are not an option: GROOVY-9588 showed
they enlarge the ATN and slow successful parses. An EOF closer on slashy
strings is also not an option: {{/}} after an expression with newlines is
division, not an unclosed slashy string.
h3. Goal
Give javac-aligned, developer-facing sentences for these common mistakes, with
an accurate caret, without reintroducing parser error alternatives on the hot
path.
h3. Approach
Error-path-only, two layers. Successful parses never enter these helpers.
* Lexer ({{GroovyLexer.g4}} / {{AbstractLexer}}): an unexpected quote becomes
*Unclosed string literal*; an unclosed block comment becomes *Unclosed comment*
at the opener (one non-greedy loop, then the closer or EOF — not a second lexer
rule, which would win by longest match and swallow trailing source). Other
unexpected characters go through {{getCharErrorDisplay}} (shared with the
GString {{$}} path from GROOVY-12171). Unicode spaces other than U+0020, curly
quotes, and non-ASCII dashes are named as a Unicode escape (for example
{{'\u200b'}}) so they do not vanish into the caret line. Stop attaching
position text on lexer {{require}} calls so {{SyntaxException}} is the only
source of {{@ line N, column M}}.
* Parser ({{AbstractFriendlyErrorStrategy}}): {{MissingDelimiterDiagnostic}}
still relocates the caret for a missing closer (GROOVY-12169). Safe index is
the same opener family as a normal index; if the innermost opener is not the
family of the sole expected closer, defer so an unclosed index inside
parentheses reports *Missing ']'*. Everything else only refines the fallback
sentence and keeps ANTLR's offending token: reserved/misplaced keyword, then
{{default:}} / {{default ->}} lookback (skip newlines; do not label an
incomplete interface {{default}} method as outside switch), then a leading
safe-index token without a receiver, then a singleton expected punctuation
token ({{Missing '('}}, {{Missing ':'}}, {{Missing '>'}}, ...), then
*Unexpected end of input* for EOF. Locate/refine stay in a defensive try;
listener dispatch is a single call afterwards.
{{AstBuilder}} reports the actual varargs parameter name.
h3. Expected result (after)
* unclosed single-, double-, or triple-quoted string becomes *Unclosed string
literal*
* unclosed block comment becomes *Unclosed comment* (caret on the opener)
* zero-width space, no-break space, curly quote, em dash become {{Unexpected
character: '\u200b'}} (and the matching escape)
* {{if true}} / {{while true}} / {{for int i in ...}} without parentheses
becomes {{Missing '('}}
* {{x ? y}} becomes {{Missing ':'}}
* a generic type missing {{>}} before {{(}} becomes {{Missing '>'}}
* {{const x = 1}} becomes {{'const' is not supported; use 'val' or 'static
final' instead}}
* {{goto label}} becomes {{'goto' is not supported}}
* {{threadsafe}} as a modifier becomes {{'threadsafe' is not supported}}
* stray {{else}} / {{catch}} / {{case}} become {{'else' without 'if'}} /
{{'catch' without 'try'}} / {{'case' outside of switch}}
* {{default: x}} / {{default -> x}} at script scope becomes {{'default' outside
of switch}}; an incomplete interface {{default}} method is not that message
* unclosed or mismatched safe index, including inside parentheses, becomes
*Missing ']'*
* a safe index with no receiver, or {{?}} immediately before one, becomes:
{noformat}
'?[' requires an expression before it
{noformat}
* a space between the question mark and the opening bracket is an incomplete
ternary ({{Missing ':'}}); safe index is one token:
{noformat}
a? [0]
{noformat}
* {{throw}} at EOF becomes *Unexpected end of input*
* {{def n = 1_}} becomes *Number ending with underscores is invalid* (position
once)
* {{def m(int... a, int b)}} becomes {{The var-arg parameter a must be the last
parameter}}
Valid programs are unchanged.
h3. Related
GROOVY-12169, GROOVY-12171, GROOVY-9588, GROOVY-10146
was:
h3. Problem
After GROOVY-12169 and GROOVY-12171, several everyday syntax mistakes still
produce a generic *Unexpected input* or *Unexpected character* message that
does not name the actual problem.
Invisible characters (zero-width space, BOM, NUL, form feed) appear as an empty
glyph in quotes. Unclosed quotes and comments are reported as an unexpected
quote or slash rather than as an unclosed literal. {{if true}} without
parentheses, {{x ? y}}, and {{const x = 1}} look like random unexpected tokens
instead of a missing {{(}} / {{:}} or an unimplemented keyword.
h3. Examples (before)
* Source:
{noformat}
println 'Hello
{noformat}
Report:
{noformat}
Unexpected character: ''' @ line 1, column 9.
{noformat}
(caret on the opening quote)
* Source:
{noformat}
/* comment
{noformat}
Report:
{noformat}
Unexpected input: '/'
{noformat}
(no mention of an unclosed comment)
* Source: {{def}} + zero-width space + {{name = null}}
Report:
{noformat}
Unexpected character: ''
{noformat}
(the offending character is invisible)
* Source:
{noformat}
if true { x = 1 }
{noformat}
Report:
{noformat}
Unexpected input: 'true'
{noformat}
* Source:
{noformat}
const x = 1
{noformat}
Report:
{noformat}
Unexpected input: 'const'
{noformat}
* Source:
{noformat}
def n = 1_
{noformat}
Report:
{noformat}
Number ending with underscores is invalid @ line 1, column 10 @ line 1, column
10.
{noformat}
(position duplicated)
* Source:
{noformat}
def m(int... a, int b) {}
{noformat}
Report:
{noformat}
The var-arg parameter strs must be the last parameter
{noformat}
(hard-coded name {{strs}})
h3. Root cause
* Lexer {{UNEXPECTED_CHAR}} inlined the raw character with only a
quote-escape, so control / format characters vanish in the message. An
unexpected quote is almost always an unclosed string, but the message never
said so.
* Unclosed block comments failed the comment rule and were retokenised as
{{/}}, so the parser saw an unexpected slash.
* Parser fallback wording was still ANTLR's *Unexpected input*, even when the
expected set was a single punctuation token (open paren, colon, {{>}}) or the
offending token was a reserved keyword ({{const}}, {{goto}}, {{else}},
{{catch}}, {{finally}}, {{case}}) or EOF.
* Lexer {{require(..., true)}} appended {{@ line N, column M}} to
{{GroovySyntaxError}}, and {{SyntaxException}} appended the same location again.
* {{AstBuilder}} used a hard-coded parameter name {{strs}} in the
varargs-not-last diagnostic.
Grammar-level parser error alternatives are not an option: GROOVY-9588 showed
they enlarge the ATN and slow successful parses.
h3. Goal
Give javac-aligned, developer-facing sentences for these common mistakes, with
an accurate caret, without reintroducing parser error alternatives on the hot
path.
h3. Approach
Error-path-only, two layers:
* Lexer ({{GroovyLexer.g4}} / {{AbstractLexer}}): unexpected quote becomes
*Unclosed string literal*; unclosed block comment becomes *Unclosed comment* at
the opener (same-rule EOF alternative, not a second lexer rule); other
unexpected characters via {{getCharErrorDisplay}} (shared with the GString
{{$}} path from GROOVY-12171). Stop attaching position text on lexer
{{require}} calls so {{SyntaxException}} is the only source of {{@ line N,
column M}}.
* Parser ({{AbstractFriendlyErrorStrategy}}): {{MissingDelimiterDiagnostic}}
still relocates the caret for a missing closer (GROOVY-12169). Everything else
only refines the fallback sentence and keeps ANTLR's offending token:
reserved/misplaced keyword, then a singleton expected punctuation token
({{Missing '('}}, {{Missing ':'}}, {{Missing '>'}}, ...), then *Unexpected end
of input* for EOF.
{{AstBuilder}} reports the actual varargs parameter name.
h3. Expected result (after)
* unclosed single-quoted string becomes *Unclosed string literal*
* unclosed block comment becomes *Unclosed comment* (caret on the opener)
* zero-width space in an identifier becomes {{Unexpected character: '\u200b'}}
* {{if true}} without parentheses becomes {{Missing '('}}
* {{x ? y}} becomes {{Missing ':'}}
* a generic type missing {{>}} before {{(}} becomes {{Missing '>'}}
* {{const x = 1}} becomes {{'const' is not supported; use 'val' or 'static
final' instead}}
* {{goto label}} becomes {{'goto' is not supported}}
* stray {{else}} / {{catch}} / {{case}} become {{'else' without 'if'}} /
{{'catch' without 'try'}} / {{'case' outside of switch}}
* {{throw}} at EOF becomes *Unexpected end of input*
* {{def n = 1_}} becomes *Number ending with underscores is invalid* (position
once)
* {{def m(int... a, int b)}} with a later parameter becomes {{The var-arg
parameter a must be the last parameter}}
Valid programs are unchanged. Successful parses never enter these helpers.
h3. Related
GROOVY-12169, GROOVY-12171, GROOVY-9588, GROOVY-10146
> Improve remaining common syntax error messages (unclosed literals, unexpected
> characters, missing punctuation, reserved keywords)
> ---------------------------------------------------------------------------------------------------------------------------------
>
> Key: GROOVY-12353
> URL: https://issues.apache.org/jira/browse/GROOVY-12353
> Project: Groovy
> Issue Type: Improvement
> Reporter: Daniel Sun
> Priority: Major
>
> h3. Problem
> After GROOVY-12169 and GROOVY-12171, several everyday syntax mistakes still
> produce a generic *Unexpected input* or *Unexpected character* message that
> does not name the actual problem.
> Invisible or look-alike characters (zero-width space, BOM, NUL, no-break
> space, curly quotes, non-ASCII dashes) render as an empty or misleading glyph
> between quotes. Unclosed quotes and comments are reported as an unexpected
> quote or slash rather than as an unclosed literal. {{if true}} without
> parentheses, {{x ? y}}, and {{const x = 1}} look like random unexpected
> tokens instead of a missing {{(}} / {{:}} or an unimplemented keyword. Groovy
> 4 safe index is a single token that pairs with a closing bracket, but an
> unclosed index inside parentheses was reported as {{Missing ')'}}.
> h3. Examples (before)
> * Unclosed string:
> {noformat}
> println 'Hello
> {noformat}
> reports:
> {noformat}
> Unexpected character: ''' @ line 1, column 9.
> {noformat}
> (caret on the opening quote)
> * Unclosed block comment:
> {noformat}
> /* comment
> {noformat}
> reports:
> {noformat}
> Unexpected input: '/'
> {noformat}
> * Invisible character ({{def}} + U+200B + {{name = null}}):
> {noformat}
> Unexpected character: ''
> {noformat}
> * Missing punctuation:
> {noformat}
> if true { x = 1 }
> {noformat}
> reports:
> {noformat}
> Unexpected input: 'true'
> {noformat}
> * Reserved keyword:
> {noformat}
> const x = 1
> {noformat}
> reports:
> {noformat}
> Unexpected input: 'const'
> {noformat}
> * Duplicate location:
> {noformat}
> def n = 1_
> {noformat}
> reports:
> {noformat}
> Number ending with underscores is invalid @ line 1, column 10 @ line 1,
> column 10.
> {noformat}
> * Hard-coded varargs name:
> {noformat}
> def m(int... a, int b) {}
> {noformat}
> reports:
> {noformat}
> The var-arg parameter strs must be the last parameter
> {noformat}
> * Unclosed safe index inside parentheses:
> {noformat}
> (a?[0
> {noformat}
> reports:
> {noformat}
> Missing ')'
> {noformat}
> h3. Root cause
> * Lexer {{UNEXPECTED_CHAR}} inlined the raw character with only a
> quote-escape, so control, format, and look-alike characters vanish or
> mislead. An unexpected quote is almost always an unclosed string, but the
> message never said so.
> * Unclosed block comments failed the comment rule and were retokenized as
> {{/}}, so the parser saw an unexpected slash.
> * Parser fallback wording was still ANTLR's *Unexpected input*, even when the
> expected set was a single punctuation token, or the offender was a reserved
> or misplaced keyword ({{const}}, {{goto}}, {{threadsafe}}, {{else}},
> {{catch}}, {{finally}}, {{case}}) or EOF. For {{default:}} / {{default ->}}
> outside a switch, the offender is {{:}} or {{->}}, not {{default}}.
> * The missing-closer diagnostic trusted a sole expected closer. For an
> unclosed safe index inside parentheses the parser expected {{)}}, so the
> inner unclosed index was reported as {{Missing ')'}}. The safe-index token
> was not treated as the same opener family as a normal index.
> * Lexer {{require(..., true)}} appended {{@ line N, column M}} to
> {{GroovySyntaxError}}, and {{SyntaxException}} appended the same location
> again.
> * {{AstBuilder}} used a hard-coded parameter name {{strs}} in the
> varargs-not-last diagnostic.
> Grammar-level parser error alternatives are not an option: GROOVY-9588 showed
> they enlarge the ATN and slow successful parses. An EOF closer on slashy
> strings is also not an option: {{/}} after an expression with newlines is
> division, not an unclosed slashy string.
> h3. Goal
> Give javac-aligned, developer-facing sentences for these common mistakes,
> with an accurate caret, without reintroducing parser error alternatives on
> the hot path.
> h3. Approach
> Error-path-only, two layers. Successful parses never enter these helpers.
> * Lexer ({{GroovyLexer.g4}} / {{AbstractLexer}}): an unexpected quote becomes
> *Unclosed string literal*; an unclosed block comment becomes *Unclosed
> comment* at the opener (one non-greedy loop, then the closer or EOF — not a
> second lexer rule, which would win by longest match and swallow trailing
> source). Other unexpected characters go through {{getCharErrorDisplay}}
> (shared with the GString {{$}} path from GROOVY-12171). Unicode spaces other
> than U+0020, curly quotes, and non-ASCII dashes are named as a Unicode escape
> (for example {{'\u200b'}}) so they do not vanish into the caret line. Stop
> attaching position text on lexer {{require}} calls so {{SyntaxException}} is
> the only source of {{@ line N, column M}}.
> * Parser ({{AbstractFriendlyErrorStrategy}}): {{MissingDelimiterDiagnostic}}
> still relocates the caret for a missing closer (GROOVY-12169). Safe index is
> the same opener family as a normal index; if the innermost opener is not the
> family of the sole expected closer, defer so an unclosed index inside
> parentheses reports *Missing ']'*. Everything else only refines the fallback
> sentence and keeps ANTLR's offending token: reserved/misplaced keyword, then
> {{default:}} / {{default ->}} lookback (skip newlines; do not label an
> incomplete interface {{default}} method as outside switch), then a leading
> safe-index token without a receiver, then a singleton expected punctuation
> token ({{Missing '('}}, {{Missing ':'}}, {{Missing '>'}}, ...), then
> *Unexpected end of input* for EOF. Locate/refine stay in a defensive try;
> listener dispatch is a single call afterwards.
> {{AstBuilder}} reports the actual varargs parameter name.
> h3. Expected result (after)
> * unclosed single-, double-, or triple-quoted string becomes *Unclosed string
> literal*
> * unclosed block comment becomes *Unclosed comment* (caret on the opener)
> * zero-width space, no-break space, curly quote, em dash become {{Unexpected
> character: '\u200b'}} (and the matching escape)
> * {{if true}} / {{while true}} / {{for int i in ...}} without parentheses
> becomes {{Missing '('}}
> * {{x ? y}} becomes {{Missing ':'}}
> * a generic type missing {{>}} before {{(}} becomes {{Missing '>'}}
> * {{const x = 1}} becomes {{'const' is not supported; use 'val' or 'static
> final' instead}}
> * {{goto label}} becomes {{'goto' is not supported}}
> * {{threadsafe}} as a modifier becomes {{'threadsafe' is not supported}}
> * stray {{else}} / {{catch}} / {{case}} become {{'else' without 'if'}} /
> {{'catch' without 'try'}} / {{'case' outside of switch}}
> * {{default: x}} / {{default -> x}} at script scope becomes {{'default'
> outside of switch}}; an incomplete interface {{default}} method is not that
> message
> * unclosed or mismatched safe index, including inside parentheses, becomes
> *Missing ']'*
> * a safe index with no receiver, or {{?}} immediately before one, becomes:
> {noformat}
> '?[' requires an expression before it
> {noformat}
> * a space between the question mark and the opening bracket is an incomplete
> ternary ({{Missing ':'}}); safe index is one token:
> {noformat}
> a? [0]
> {noformat}
> * {{throw}} at EOF becomes *Unexpected end of input*
> * {{def n = 1_}} becomes *Number ending with underscores is invalid*
> (position once)
> * {{def m(int... a, int b)}} becomes {{The var-arg parameter a must be the
> last parameter}}
> Valid programs are unchanged.
> h3. Related
> GROOVY-12169, GROOVY-12171, GROOVY-9588, GROOVY-10146
--
This message was sent by Atlassian Jira
(v8.20.10#820010)