@@ -50,6 +50,12 @@ export function createLexer(grammar: CstGrammar) {
5050 const divisionPrevTypes = new Set ( [ ...( regexCtx ?. divisionAfterTypes ?? [ ] ) , '$templateTail' ] ) ;
5151 const divisionPrevTexts = new Set ( regexCtx ?. divisionAfterTexts ?? [ ] ) ;
5252 const expressionStartKeywords = new Set ( regexCtx ?. regexAfterTexts ?? [ ] ) ;
53+ // Keywords that head a `kw ( … )` control group; the matching `)` is a statement
54+ // head (not a value), so a following `/` is a regex, not division.
55+ const parenHeadKeywords = new Set ( regexCtx ?. regexAfterParenKeywords ?? [ ] ) ;
56+ // Member-access texts (`.`/`?.`): a keyword right after one is a property name, so
57+ // the control-head rule above does not apply (`obj.for(x) / y` is a call/division).
58+ const memberAccessTexts = new Set ( regexCtx ?. memberAccessTexts ?? [ ] ) ;
5359
5460 // Scan from inside a template span to its next boundary: an interpolation hole
5561 // (`interpOpen`) or the closing delimiter (`open`). Delimiters come from the
@@ -73,6 +79,11 @@ export function createLexer(grammar: CstGrammar) {
7379 const tokens : Token [ ] = [ ] ;
7480 let pos = 0 ;
7581 const templateStack : number [ ] = [ ] ;
82+ // For each open `(`, whether it heads a control group (`if`/`while`/…) so the
83+ // matching `)` is a statement head, not a value. `lastCloseWasParenHead` carries
84+ // that to the regex-vs-division check (consulted only when prev is `)`).
85+ const parenHeadStack : boolean [ ] = [ ] ;
86+ let lastCloseWasParenHead = false ;
7687
7788 while ( pos < source . length ) {
7889 // Skip whitespace
@@ -131,7 +142,9 @@ export function createLexer(grammar: CstGrammar) {
131142 if ( prev ) {
132143 // Expression-start keywords (in, throw, return, etc.) flip back to regex context
133144 const isExprKeyword = prev . type === identTokenName && expressionStartKeywords . has ( prev . text ) ;
134- if ( ! isExprKeyword && ( divisionPrevTypes . has ( prev . type ) || divisionPrevTexts . has ( prev . text ) ) ) {
145+ // A `)` that closed a control head (`if (…) /re/`) is not a value → regex.
146+ const isParenHead = prev . text === ')' && lastCloseWasParenHead ;
147+ if ( ! isExprKeyword && ! isParenHead && ( divisionPrevTypes . has ( prev . type ) || divisionPrevTexts . has ( prev . text ) ) ) {
135148 continue ;
136149 }
137150 }
@@ -151,6 +164,20 @@ export function createLexer(grammar: CstGrammar) {
151164 // Try punctuation literals (longest first)
152165 for ( const lit of punctLiterals ) {
153166 if ( remaining . startsWith ( lit ) ) {
167+ // Track control-head parens so a `/` after `if (…)`/`while (…)` is a regex.
168+ // The keyword must be a real keyword head, not a member name: `obj.for(x) / y`
169+ // is a method call + division, so skip when the keyword is itself preceded by
170+ // a member accessor (e.g. `.`/`?.`, from divisionAfterTexts) → property access.
171+ if ( lit === '(' ) {
172+ const prev = tokens [ tokens . length - 1 ] ;
173+ const beforePrev = tokens [ tokens . length - 2 ] ;
174+ const isMemberName = ! ! beforePrev && memberAccessTexts . has ( beforePrev . text ) ;
175+ parenHeadStack . push (
176+ ! isMemberName && ! ! prev && prev . type === identTokenName && parenHeadKeywords . has ( prev . text ) ,
177+ ) ;
178+ } else if ( lit === ')' ) {
179+ lastCloseWasParenHead = parenHeadStack . pop ( ) ?? false ;
180+ }
154181 tokens . push ( { type : '' , text : lit , offset : pos } ) ;
155182 pos += lit . length ;
156183 matched = true ;
0 commit comments