[ 
https://issues.apache.org/jira/browse/GROOVY-12425?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18128648#comment-18128648
 ] 

ASF GitHub Bot commented on GROOVY-12425:
-----------------------------------------

paulk-asert commented on PR #2944:
URL: https://github.com/apache/groovy/pull/2944#issuecomment-6262890990

   This looks good with some minor suggested improvements:
   
   > **Verdict: Careful, well-tested, and the reasoning is documented in the 
code itself rather than in a report. One genuine false-positive risk in the 
lexer that I'd want closed before merge, plus one scope gap and one bit of 
unrelated scope creep.**
   > 
   > ## What it changes
   > 
   > Two unrelated diagnostics, plus a drive-by:
   > 
   > 1. **Unclosed slashy string.** `s = /hello` reported `Unexpected input: 
'/'` because `/` is also `DIV`, so an unclosed `/.../` fell through to the 
division token. A new EOF alternative in the `StringLiteral` lexer rule now 
yields `Unclosed string literal` with the caret on the opening `/`.
   > 2. **Unclosed type argument.** `List<Integer name` already said `Missing 
'>'`, but the caret sat on `List` — often many tokens from the actual problem. 
It now points at the first token of the argument list.
   > 3. `Objects.requireNonNull` in `transformRecordHeaderToProperties` — 
unrelated to this JIRA.
   > 
   > ## The lexer change — what I verified
   > 
   > This is the part that could break working code, so I traced it against the 
grammar on master.
   > 
   > ```
   > |   Slash { this.isRegexAllowed() && _input.LA(1) != '*' }?
   >         (   SlashyStringCharacter+ Slash
   >         |   { this.isUnclosedSlashy() }? SlashyStringCharacter* EOF { 
requireUnclosedString(errorIgnored); }
   >         )
   > ```
   > 
   > - **`a / b` is untouched.** The outer predicate `isRegexAllowed()` already 
rejects the whole slashy alternative when the preceding token is a division 
operand (`Identifier` is in `REGEX_CHECK_SET`), so the new alternative is never 
even reached.
   > - **`9 \n / \n 3` stays division — and this is the subtle part the PR gets 
right.** `NL` is *not* in `REGEX_CHECK_SET`, so `isRegexAllowed()` returns true 
after a newline and the slashy alternative *is* attempted. Without more, that 
input would become a bogus "unclosed string literal". That's exactly why 
`lastRealTokenType` exists: `isUnclosedSlashy()` looks *through* `NL` to the 
last real token (`9`, an `IntegerLiteral`, which is an operand) and rejects the 
alternative. The deliberate asymmetry — `isRegexAllowed()` keeps using 
`lastTokenType` so a *closed* regex can still start a line, while 
`isUnclosedSlashy()` is stricter — is correct and is the right way round.
   > - **`//` and `/*` can't match.** `/` is not a `SlashyStringCharacter` 
(only `\/` via `SlashEscape`), so an empty body followed by a second `/` fails 
the `EOF` requirement; `/*` is excluded by the existing `LA(1) != '*'` guard.
   > - **A real closer is never swallowed.** Since the body excludes `/`, 
alternative 1 always wins when a closing `/` exists on the same token.
   > - **Slashy GStrings are not broken.** `SlashyStringCharacter` admits `$` 
only when *not* followed by a Java letter, so the new alternative cannot 
consume `/hello $name` and `SlashyGStringBegin` still wins.
   > - **`Token.EOF` is guarded twice** — in `emit()` and in 
`isDivisionOperand()` — because `BitSet.get(-1)` throws. `Token.INVALID_TYPE` 
is 0, identical to the old implicit default, so making it explicit changes 
nothing.
   > - **The `reset()` override is a latent bug fix.** `Lexer.setInputStream` 
calls `reset()`, and previously a reused lexer carried `lastTokenType` across 
inputs, so a leading `/` in a second input was classified using the *first* 
input's last token. There's a test for it 
(`reusedLexerForgetsThePreviousDivisionOperand`). Good catch, and worth calling 
out separately in the JIRA.
   > - **Predicate placement is correct.** `isUnclosedSlashy()` is on the left 
edge of the alternative, which is where ANTLR can reliably evaluate a lexer 
predicate during ATN simulation.
   > 
   > ## The one real risk
   > 
   > **`REGEX_CHECK_SET` is a permissive heuristic, and this change promotes it 
into the justification for a hard error.**
   > 
   > The set contains: `DEC, INC, THIS, RBRACE, RBRACK, RPAREN, GStringEnd, 
NullLiteral, StringLiteral, BooleanLiteral, IntegerLiteral, 
FloatingPointLiteral, Identifier, CapitalizedIdentifier`.
   > 
   > It was built to answer "could a regex plausibly start here?" — and a gap 
in it previously meant only a mislex requiring *two* slashes. The new EOF 
alternative uses the complement of that same set to assert "this definitely 
cannot be division, so emit an error." Any value-yielding token missing from 
the set now turns **valid code into a compile error**.
   > 
   > Notably absent are the keyword tokens that can terminate a value 
expression via property access. `GroovyParser.g4:1305+` lists `CLASS`, 
`DEFAULT`, `IN`, `TRAIT`, `RECORD`, `VAR`, `YIELD` and others as usable name 
parts, and the lexer still emits them as keyword tokens. So:
   > 
   > ```groovy
   > def n = size.class / 2        // last '/' in the file
   > ```
   > 
   > `lastTokenType` is `CLASS`, which is not in the set, so `isRegexAllowed()` 
passes, `isUnclosedSlashy()` passes, and the EOF alternative matches `/ 2<EOF>` 
→ `Unclosed string literal`. Today that same input lexes `/` as `DIV` and 
compiles.
   > 
   > Three things bound the exposure, and they bound it a lot:
   > 1. The `/` must be the **last `/` in the file** (the body excludes `/`, so 
EOF must arrive first).
   > 2. The preceding token must be outside `REGEX_CHECK_SET`.
   > 3. It must be genuine division.
   > 
   > So this is narrow and my examples are contrived — dividing a `Class` isn't 
meaningful. But the failure mode is the worst kind for a diagnostics PR: 
previously-compiling code now fails to compile, and the error message actively 
misdirects. I derived this from reading the grammar rather than running it, so 
the right next step is a test, not a redesign.
   > 
   > The fix is cheap and strictly improves things: add the value-terminating 
keyword tokens to `REGEX_CHECK_SET`. That also fixes the *pre-existing* 
two-slash misparse (`a.class /2/ b` is already mislexed as a regex on master). 
Alternatively, gate the EOF alternative on a stricter positive whitelist rather 
than the complement of a permissive set.
   > 
   > ## Scope gap: slashy GStrings
   > 
   > An unclosed *interpolated* slashy — `s = /hello $name` — is not covered. 
`SlashyGStringBegin` matches `/hello $`, enters `SLASHY_GSTRING_MODE`, never 
finds a closer, and falls back to the old error. The user-facing mistake is 
identical to the plain case, so the message will now be inconsistent between 
`/hello` and `/hello $name`.
   > 
   > The JIRA explicitly documents and justifies excluding `$/...$/` (an EOF 
alternative there would reject `($/2)`, `$/=`, `$//`) — that reasoning is 
sound. But it doesn't mention the slashy-GString case, and there's no test 
pinning the behaviour either way. I'd want it documented as a known limitation 
with a test asserting whatever it currently does, so the gap is explicit rather 
than accidental.
   > 
   > ## Scope creep
   > 
   > ```java
   > Parameter[] parameters = 
Objects.requireNonNull(this.visitFormalParameters(ctx.formalParameters()));
   > ```
   > 
   > This is in `transformRecordHeaderToProperties` — records, nothing to do 
with error messages — and the comment concedes the value can never be null 
(`visitFormalParameters` returns `Parameter.EMPTY_ARRAY`). It exists to silence 
`javabugs:S2259`. Adding a runtime check that provably cannot fire, to appease 
a static analyser, in an unrelated PR, is three small negatives at once. I'd 
drop it or split it into its own commit.
   > 
   > ## Consumer-visible behaviour changes
   > 
   > Both are expected for this kind of change, but worth being deliberate 
about:
   > 
   > - **The `Missing '>'` caret moves** (column 1 → column 16 for `List < 
Integer name`). Anything asserting on full error output — IDE integration 
tests, tooling that parses compiler output — will see a diff. The existing 
tests were updated accordingly.
   > - **In `errorIgnored` mode the token stream changes.** An unclosed `/...` 
now lexes as a single `StringLiteral` instead of `DIV` plus identifiers. That's 
better for syntax highlighting, but Groovy-Eclipse consumes this lexer, so it's 
a real downstream change. There is a test 
(`errorIgnoredUnclosedSlashyTokenizesWithoutThrowing`) confirming tokenisation 
completes and reaches EOF.
   > 
   > ## Test quality — the strongest part
   > 
   > The tests map one-to-one onto the risks, which is unusual and good. 
Specifically covered: `a / b` stays division; `$/ab/$` still works; `//` is not 
an empty slashy; a closed slashy after a newline is still a string; `9 \n / \n 
3` stays division (the `lastRealTokenType` path); a leading newline *without* 
an operand *is* unclosed; a reused lexer forgets the previous operand (the 
`reset()` path); `errorIgnored` doesn't throw; and caret positions at 
start-of-file, mid-file, after a line break, and at bare `/`.
   > 
   > Gaps: no symmetric "valid division survives" case for a token *outside* 
`REGEX_CHECK_SET` (the risk above), and nothing pinning slashy-GString 
behaviour.
   > 
   > ## Recommendation
   > 
   > Merge once the `REGEX_CHECK_SET` gap is closed — add the value-terminating 
keyword tokens plus a test asserting `x.class / 2` at end-of-file still 
compiles. Add a test pinning unclosed-slashy-GString behaviour and note it as a 
limitation in the JIRA. Drop or split the `Objects.requireNonNull` change. The 
lexer reasoning and the `reset()` fix are both good work and the latter 
deserves its own mention in the JIRA rather than riding along silently.




> Refine error message for unclosed type argument and unclosed slashy string
> --------------------------------------------------------------------------
>
>                 Key: GROOVY-12425
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12425
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Daniel Sun
>            Priority: Major
>
> h2. Description
> Two parser diagnostics name a missing closer but point at the wrong token, or 
> name a different mistake. Both should match the caret already used for an 
> unclosed quote: one sentence, on the token that is wrong.
> h3. Unclosed type argument
> {{List<Integer name}} is parsed as the comparison {{List < Integer}} plus a 
> following argument list. The message was already {{Missing '>'}}, which is 
> the same advice as javac's {{> or ',' expected}}. The caret sat on {{List}}, 
> often many tokens away from the missing {{>}}.
> The caret now sits on the first token of that argument list. An identifier 
> after {{T < U}} is an argument list, not a command argument, so the caret is 
> the start of the list. Extra spaces do not pull it back onto the space after 
> the type argument.
> * {{List<Integer list2}} — caret on {{list2}}
> * {{List<List<Integer> list2}} — caret on {{list2}}, after the inner {{>}}
> * {{List<Integer  name}} — two spaces; caret on {{name}}, not on the first 
> space
> * {{List < Integer 1, 2}} — caret on {{1}}
> * {{List < Integer name foo}} — caret on {{name}}, not on {{foo}}
> A comparison that is not two type names stays {{Unexpected input}}. {{list < 
> Integer name}} is one of those.
> {noformat}
> Missing '>' @ line 1, column 14.
>    List<Integer list2 = new ArrayList<Integer>()
>                 ^
> {noformat}
> h3. Unclosed slashy string
> An unclosed {{/.../}} was reported as {{Unexpected input: '/'}}. {{/}} is 
> also the division operator, so a slashy string that never closes fell through 
> to that token.
> It is now {{Unclosed string literal}}, with the caret on the opening {{/}}. 
> That is the same sentence and the same opener caret as an unclosed {{'}} or 
> {{"}}, and the same place javac marks {{unclosed string literal}}.
> {noformat}
> Unclosed string literal @ line 1, column 5.
>    s = /hello
>        ^
> {noformat}
> The same message is used when the slashy string is only {{/}} at end of 
> input, and when the opener is on the line after {{=}}.
> Division is unchanged:
> * {{a / b}} stays division.
> * A value, a line break, then {{/}}, then another value stays division. A 
> newline is not a left operand. {{9}} split across lines from {{3}} by {{/}} 
> still divides.
> * A closed {{/ab/}} is still a string. {{//}} and {{/*}} are still comments.
> {{$/}} is not treated as an unclosed string. {{$}} is an identifier, so 
> {{($/2)}}, {{$/=}}, and {{$//}} are division. An end-of-input alternative on 
> {{$/}} would reject those programs. An unclosed {{$/...}} therefore still 
> surfaces as a later error.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to