@@ -172,9 +172,17 @@ type CodeScanMode =
172172 * a string, and every reference past that point was then formatted for the wrong context.
173173 * Division always follows a value — an identifier, literal, `)`, or `]` — so anything else
174174 * ending the preceding token means a regex may start.
175+ *
176+ * `)` is the one that is not decidable from the character alone: it ends a value in
177+ * `(a + b) / 2` and a control-flow head in `if (a) /re/.test(b)`. Reading it as either one
178+ * unconditionally breaks the other, so the scan remembers which kind of parenthesis each `)`
179+ * closed rather than guessing — see {@link CONTROL_FLOW_HEAD_KEYWORDS}.
175180 */
176181const JAVASCRIPT_REGEX_ALLOWED_AFTER = new Set ( '(,=:[!&|?{};+-*%^~<>/' . split ( '' ) )
177182
183+ /** Keywords whose parenthesized head is followed by a statement, where a regex may start. */
184+ const CONTROL_FLOW_HEAD_KEYWORDS = new Set ( [ 'if' , 'while' , 'for' , 'switch' , 'catch' , 'with' ] )
185+
178186const WHITESPACE_CHAR = / \s /
179187
180188/** Keywords a regex may directly follow, where the preceding token is a word rather than punctuation. */
@@ -1118,11 +1126,18 @@ export class VariableResolver {
11181126 * anything else means a regex may start. Guessing wrong is not silent — the scan would swallow
11191127 * text up to the next `/` — so the check reads the actual preceding token rather than assuming.
11201128 */
1121- private canStartJavaScriptRegex ( template : string , previousSignificantIndex : number ) : boolean {
1129+ private canStartJavaScriptRegex (
1130+ template : string ,
1131+ previousSignificantIndex : number ,
1132+ controlHeadParenCloses : ReadonlySet < number >
1133+ ) : boolean {
11221134 if ( previousSignificantIndex < 0 ) {
11231135 return true
11241136 }
11251137 const previous = template [ previousSignificantIndex ]
1138+ if ( previous === ')' ) {
1139+ return controlHeadParenCloses . has ( previousSignificantIndex )
1140+ }
11261141 if ( JAVASCRIPT_REGEX_ALLOWED_AFTER . has ( previous ) ) {
11271142 return true
11281143 }
@@ -1139,6 +1154,26 @@ export class VariableResolver {
11391154 )
11401155 }
11411156
1157+ /**
1158+ * Whether the `(` at this index opens a control-flow head rather than a value.
1159+ *
1160+ * What follows the matching `)` differs entirely between the two — a statement, where a regex
1161+ * literal may begin, versus an operator, where a `/` divides — and the closing parenthesis
1162+ * carries no trace of which it was. Reading the keyword in front of the opening one is what
1163+ * lets `if (a) /re/.test(b)` and `(a + b) / 2` both scan correctly.
1164+ */
1165+ private opensControlFlowHead ( template : string , index : number ) : boolean {
1166+ let end = index
1167+ while ( end > 0 && WHITESPACE_CHAR . test ( template [ end - 1 ] ) ) {
1168+ end --
1169+ }
1170+ let start = end
1171+ while ( start > 0 && this . isJavaScriptIdentifierChar ( template [ start - 1 ] ) ) {
1172+ start --
1173+ }
1174+ return CONTROL_FLOW_HEAD_KEYWORDS . has ( template . slice ( start , end ) )
1175+ }
1176+
11421177 private matchesKeywordAt ( template : string , index : number , keyword : string ) : boolean {
11431178 if ( ! template . startsWith ( keyword , index ) ) {
11441179 return false
@@ -1267,6 +1302,8 @@ export class VariableResolver {
12671302 const isPython = language === 'python'
12681303 const modes : CodeScanMode [ ] = [ { type : 'normal' } ]
12691304 let lastSignificantIndex = - 1
1305+ const openParenIsControlHead : boolean [ ] = [ ]
1306+ const controlHeadParenCloses = new Set < number > ( )
12701307
12711308 for ( let i = 0 ; i < index ; i ++ ) {
12721309 const char = template [ i ]
@@ -1369,10 +1406,15 @@ export class VariableResolver {
13691406 if ( ! WHITESPACE_CHAR . test ( char ) ) {
13701407 lastSignificantIndex = i
13711408 }
1409+ if ( char === '(' ) {
1410+ openParenIsControlHead . push ( this . opensControlFlowHead ( template , i ) )
1411+ } else if ( char === ')' ) {
1412+ if ( openParenIsControlHead . pop ( ) ) controlHeadParenCloses . add ( i )
1413+ }
13721414 if (
13731415 ! isPython &&
13741416 char === '/' &&
1375- this . canStartJavaScriptRegex ( template , previousSignificantIndex )
1417+ this . canStartJavaScriptRegex ( template , previousSignificantIndex , controlHeadParenCloses )
13761418 ) {
13771419 modes . push ( { type : 'regex' , inCharacterClass : false } )
13781420 continue
@@ -1430,10 +1472,15 @@ export class VariableResolver {
14301472 if ( ! WHITESPACE_CHAR . test ( char ) ) {
14311473 lastSignificantIndex = i
14321474 }
1475+ if ( char === '(' ) {
1476+ openParenIsControlHead . push ( this . opensControlFlowHead ( template , i ) )
1477+ } else if ( char === ')' ) {
1478+ if ( openParenIsControlHead . pop ( ) ) controlHeadParenCloses . add ( i )
1479+ }
14331480 if (
14341481 ! isPython &&
14351482 char === '/' &&
1436- this . canStartJavaScriptRegex ( template , previousSignificantIndex )
1483+ this . canStartJavaScriptRegex ( template , previousSignificantIndex , controlHeadParenCloses )
14371484 ) {
14381485 modes . push ( { type : 'regex' , inCharacterClass : false } )
14391486 continue
0 commit comments