@@ -146,7 +146,12 @@ function isStructurallyInertConditionLiteral(value: string): boolean {
146146}
147147
148148type ShellQuoteContext = 'single' | 'double' | null
149- type CodeStringQuoteContext = ShellQuoteContext | 'triple-single' | 'triple-double' | 'template'
149+ type CodeStringQuoteContext =
150+ | ShellQuoteContext
151+ | 'triple-single'
152+ | 'triple-double'
153+ | 'template'
154+ | 'regex'
150155type CodeScanMode =
151156 | { type : 'normal' }
152157 | { type : 'single' }
@@ -157,6 +162,37 @@ type CodeScanMode =
157162 | { type : 'template-expression' ; depth : number }
158163 | { type : 'line-comment' }
159164 | { type : 'block-comment' }
165+ | { type : 'regex' ; inCharacterClass : boolean }
166+
167+ /**
168+ * Characters after which a `/` opens a regular expression rather than dividing.
169+ *
170+ * The scanner has to tell the two apart, because a regex body is the one place a lone quote
171+ * is not a string delimiter: `/['"]/` left the scan believing everything after it sat inside
172+ * a string, and every reference past that point was then formatted for the wrong context.
173+ * Division always follows a value — an identifier, literal, `)`, or `]` — so anything else
174+ * ending the preceding token means a regex may start.
175+ */
176+ const JAVASCRIPT_REGEX_ALLOWED_AFTER = new Set ( '(,=:[!&|?{};+-*%^~<>/' . split ( '' ) )
177+
178+ const WHITESPACE_CHAR = / \s /
179+
180+ /** Keywords a regex may directly follow, where the preceding token is a word rather than punctuation. */
181+ const JAVASCRIPT_REGEX_ALLOWED_AFTER_KEYWORDS = new Set ( [
182+ 'return' ,
183+ 'typeof' ,
184+ 'instanceof' ,
185+ 'in' ,
186+ 'of' ,
187+ 'new' ,
188+ 'delete' ,
189+ 'void' ,
190+ 'case' ,
191+ 'do' ,
192+ 'else' ,
193+ 'yield' ,
194+ 'await' ,
195+ ] )
160196
161197export class VariableResolver {
162198 private resolvers : Resolver [ ]
@@ -1073,6 +1109,35 @@ export class VariableResolver {
10731109 )
10741110 }
10751111
1112+ /**
1113+ * Whether a `/` at this point opens a regular expression rather than dividing.
1114+ *
1115+ * Division always follows a value, so the preceding token decides: an identifier that is not
1116+ * one of the keywords a regex may follow, a number, a `)`, or a `]` means division, and
1117+ * anything else means a regex may start. Guessing wrong is not silent — the scan would swallow
1118+ * text up to the next `/` — so the check reads the actual preceding token rather than assuming.
1119+ */
1120+ private canStartJavaScriptRegex ( template : string , previousSignificantIndex : number ) : boolean {
1121+ if ( previousSignificantIndex < 0 ) {
1122+ return true
1123+ }
1124+ const previous = template [ previousSignificantIndex ]
1125+ if ( JAVASCRIPT_REGEX_ALLOWED_AFTER . has ( previous ) ) {
1126+ return true
1127+ }
1128+ if ( ! this . isJavaScriptIdentifierChar ( previous ) ) {
1129+ return false
1130+ }
1131+
1132+ let start = previousSignificantIndex
1133+ while ( start > 0 && this . isJavaScriptIdentifierChar ( template [ start - 1 ] ) ) {
1134+ start --
1135+ }
1136+ return JAVASCRIPT_REGEX_ALLOWED_AFTER_KEYWORDS . has (
1137+ template . slice ( start , previousSignificantIndex + 1 )
1138+ )
1139+ }
1140+
10761141 private matchesKeywordAt ( template : string , index : number , keyword : string ) : boolean {
10771142 if ( ! template . startsWith ( keyword , index ) ) {
10781143 return false
@@ -1200,6 +1265,7 @@ export class VariableResolver {
12001265 ) : CodeStringQuoteContext {
12011266 const isPython = language === 'python'
12021267 const modes : CodeScanMode [ ] = [ { type : 'normal' } ]
1268+ let lastSignificantIndex = - 1
12031269
12041270 for ( let i = 0 ; i < index ; i ++ ) {
12051271 const char = template [ i ]
@@ -1221,6 +1287,31 @@ export class VariableResolver {
12211287 continue
12221288 }
12231289
1290+ if ( mode . type === 'regex' ) {
1291+ if ( char === '\\' ) {
1292+ i ++
1293+ continue
1294+ }
1295+ // A regex literal cannot span a line, so an unterminated one means the `/` was
1296+ // division after all; dropping the mode keeps the rest of the scan honest.
1297+ if ( char === '\n' ) {
1298+ modes . pop ( )
1299+ continue
1300+ }
1301+ if ( char === '[' ) {
1302+ mode . inCharacterClass = true
1303+ continue
1304+ }
1305+ if ( char === ']' ) {
1306+ mode . inCharacterClass = false
1307+ continue
1308+ }
1309+ if ( char === '/' && ! mode . inCharacterClass ) {
1310+ modes . pop ( )
1311+ }
1312+ continue
1313+ }
1314+
12241315 if ( mode . type === 'single' || mode . type === 'double' ) {
12251316 const quote = mode . type === 'single' ? "'" : '"'
12261317 if ( char === '\\' ) {
@@ -1273,6 +1364,18 @@ export class VariableResolver {
12731364 i ++
12741365 continue
12751366 }
1367+ const previousSignificantIndex = lastSignificantIndex
1368+ if ( ! WHITESPACE_CHAR . test ( char ) ) {
1369+ lastSignificantIndex = i
1370+ }
1371+ if (
1372+ ! isPython &&
1373+ char === '/' &&
1374+ this . canStartJavaScriptRegex ( template , previousSignificantIndex )
1375+ ) {
1376+ modes . push ( { type : 'regex' , inCharacterClass : false } )
1377+ continue
1378+ }
12761379 if ( isPython && char === "'" && next === "'" && template [ i + 2 ] === "'" ) {
12771380 modes . push ( { type : 'triple-single' } )
12781381 i += 2
@@ -1322,6 +1425,18 @@ export class VariableResolver {
13221425 i ++
13231426 continue
13241427 }
1428+ const previousSignificantIndex = lastSignificantIndex
1429+ if ( ! WHITESPACE_CHAR . test ( char ) ) {
1430+ lastSignificantIndex = i
1431+ }
1432+ if (
1433+ ! isPython &&
1434+ char === '/' &&
1435+ this . canStartJavaScriptRegex ( template , previousSignificantIndex )
1436+ ) {
1437+ modes . push ( { type : 'regex' , inCharacterClass : false } )
1438+ continue
1439+ }
13251440 if ( isPython && char === "'" && next === "'" && template [ i + 2 ] === "'" ) {
13261441 modes . push ( { type : 'triple-single' } )
13271442 i += 2
@@ -1338,6 +1453,9 @@ export class VariableResolver {
13381453 }
13391454
13401455 const mode = modes [ modes . length - 1 ]
1456+ if ( mode . type === 'regex' ) {
1457+ return 'regex'
1458+ }
13411459 if (
13421460 mode . type === 'single' ||
13431461 mode . type === 'double' ||
0 commit comments