@@ -1317,6 +1317,184 @@ const REGEX_ALLOWED_AFTER = new Set([
13171317 '\n' ,
13181318] )
13191319
1320+ /**
1321+ * Keywords after which a `/` begins a regex literal rather than a division. In every one of
1322+ * these positions an operand is expected, so `return /x/`, `typeof /x/` or `case /x/` opens a
1323+ * regex — a check on the previous character alone reads the keyword's last letter as an
1324+ * identifier and lexes the `/` as division.
1325+ */
1326+ const REGEX_START_KEYWORDS = new Set ( [
1327+ 'return' ,
1328+ 'typeof' ,
1329+ 'case' ,
1330+ 'in' ,
1331+ 'of' ,
1332+ 'new' ,
1333+ 'delete' ,
1334+ 'void' ,
1335+ 'instanceof' ,
1336+ 'do' ,
1337+ 'else' ,
1338+ 'yield' ,
1339+ 'await' ,
1340+ ] )
1341+
1342+ /**
1343+ * Whether the word immediately before `index` is a {@link REGEX_START_KEYWORDS} keyword.
1344+ * A property access (`counts.in / 2`) is excluded, since there the word is an identifier
1345+ * and the `/` really is a division.
1346+ */
1347+ function precededByRegexStartKeyword ( content : string , index : number ) : boolean {
1348+ let j = index - 1
1349+ while ( j >= 0 && / \s / . test ( content [ j ] ) ) j --
1350+ const wordEnd = j + 1
1351+ while ( j >= 0 && / [ A - Z a - z 0 - 9 _ $ ] / . test ( content [ j ] ) ) j --
1352+ if ( ! REGEX_START_KEYWORDS . has ( content . slice ( j + 1 , wordEnd ) ) ) return false
1353+ return content [ j ] !== '.' && content [ j ] !== '#'
1354+ }
1355+
1356+ /** Index just past a `//` comment opening at `start`. */
1357+ function scanLineComment ( content : string , start : number ) : number {
1358+ const newline = content . indexOf ( '\n' , start )
1359+ return newline === - 1 ? content . length : newline
1360+ }
1361+
1362+ /** Index just past the block comment opening at `start`, or null when it never closes. */
1363+ function scanBlockComment ( content : string , start : number ) : number | null {
1364+ const close = content . indexOf ( '*/' , start + 2 )
1365+ return close === - 1 ? null : close + 2
1366+ }
1367+
1368+ /** Index just past a regex literal (and its flags) opening at `start`, or null when unterminated. */
1369+ function scanRegexLiteral ( content : string , start : number ) : number | null {
1370+ let j = start + 1
1371+ let inClass = false
1372+ let closed = false
1373+ while ( j < content . length ) {
1374+ const c = content [ j ]
1375+ if ( c === '\\' ) {
1376+ j += 2
1377+ continue
1378+ }
1379+ if ( c === '\n' ) break
1380+ if ( c === '[' ) inClass = true
1381+ else if ( c === ']' ) inClass = false
1382+ else if ( c === '/' && ! inClass ) {
1383+ closed = true
1384+ break
1385+ }
1386+ j ++
1387+ }
1388+ if ( ! closed ) return null
1389+ j ++
1390+ while ( j < content . length && / [ a - z ] / . test ( content [ j ] ) ) j ++
1391+ return j
1392+ }
1393+
1394+ /** Index OF the closing quote of the string opening at `start`, or null when unterminated. */
1395+ function scanQuoted ( content : string , start : number ) : number | null {
1396+ const quote = content [ start ]
1397+ let j = start + 1
1398+ while ( j < content . length ) {
1399+ if ( content [ j ] === '\\' ) {
1400+ j += 2
1401+ continue
1402+ }
1403+ if ( content [ j ] === '\n' ) break
1404+ if ( content [ j ] === quote ) return j
1405+ j ++
1406+ }
1407+ return null
1408+ }
1409+
1410+ /** Index OF the closing backtick of the template literal opening at `start`, or null. */
1411+ function scanTemplateLiteral ( content : string , start : number ) : number | null {
1412+ let j = start + 1
1413+ while ( j < content . length ) {
1414+ const c = content [ j ]
1415+ if ( c === '\\' ) {
1416+ j += 2
1417+ continue
1418+ }
1419+ if ( c === '`' ) return j
1420+ if ( c === '$' && content [ j + 1 ] === '{' ) {
1421+ const end = scanTemplateExpression ( content , j + 2 )
1422+ if ( end === null ) return null
1423+ j = end
1424+ continue
1425+ }
1426+ j ++
1427+ }
1428+ return null
1429+ }
1430+
1431+ /**
1432+ * Index just past the `}` that closes the `${` expression starting at `start`, or null.
1433+ *
1434+ * The expression is lexed with the same primitives as top-level code, so a brace inside a
1435+ * nested string, comment, regex or template never counts toward the depth — a plain counter
1436+ * loses the closing backtick of `` `${ f("}") }` `` and reports the whole block unreadable.
1437+ */
1438+ function scanTemplateExpression ( content : string , start : number ) : number | null {
1439+ let j = start
1440+ let depth = 0
1441+ let prevSignificant = ''
1442+ while ( j < content . length ) {
1443+ const c = content [ j ]
1444+
1445+ if ( c === '/' && content [ j + 1 ] === '/' ) {
1446+ j = scanLineComment ( content , j )
1447+ continue
1448+ }
1449+ if ( c === '/' && content [ j + 1 ] === '*' ) {
1450+ const end = scanBlockComment ( content , j )
1451+ if ( end === null ) return null
1452+ j = end
1453+ continue
1454+ }
1455+ if ( c === '/' && startsRegexLiteral ( content , j , prevSignificant ) ) {
1456+ const end = scanRegexLiteral ( content , j )
1457+ if ( end === null ) return null
1458+ prevSignificant = ')'
1459+ j = end
1460+ continue
1461+ }
1462+ if ( c === "'" || c === '"' ) {
1463+ const close = scanQuoted ( content , j )
1464+ if ( close === null ) return null
1465+ prevSignificant = c
1466+ j = close + 1
1467+ continue
1468+ }
1469+ if ( c === '`' ) {
1470+ const close = scanTemplateLiteral ( content , j )
1471+ if ( close === null ) return null
1472+ prevSignificant = '`'
1473+ j = close + 1
1474+ continue
1475+ }
1476+ if ( c === '{' ) depth ++
1477+ else if ( c === '}' ) {
1478+ if ( depth === 0 ) return j + 1
1479+ depth --
1480+ }
1481+
1482+ if ( ! / \s / . test ( c ) ) prevSignificant = c
1483+ else if ( c === '\n' ) prevSignificant = '\n'
1484+ j ++
1485+ }
1486+ return null
1487+ }
1488+
1489+ /** Whether the `/` at `index` opens a regex literal rather than a division. */
1490+ function startsRegexLiteral ( content : string , index : number , prevSignificant : string ) : boolean {
1491+ return (
1492+ prevSignificant === '' ||
1493+ REGEX_ALLOWED_AFTER . has ( prevSignificant ) ||
1494+ precededByRegexStartKeyword ( content , index )
1495+ )
1496+ }
1497+
13201498/**
13211499 * Blank out string literals, template literals, comments and regex literals so a structural
13221500 * scan sees only code punctuation. Length and newlines are preserved, which the `readLiteral`
@@ -1342,97 +1520,44 @@ function blankStringsAndComments(content: string): string | null {
13421520 const char = content [ i ]
13431521
13441522 if ( char === '/' && content [ i + 1 ] === '/' ) {
1345- const nl = content . indexOf ( '\n' , i )
1346- const end = nl === - 1 ? content . length : nl
1523+ const end = scanLineComment ( content , i )
13471524 blank ( i , end )
13481525 i = end
13491526 continue
13501527 }
13511528
13521529 if ( char === '/' && content [ i + 1 ] === '*' ) {
1353- const close = content . indexOf ( '*/' , i + 2 )
1354- if ( close === - 1 ) return null
1355- blank ( i , close + 2 )
1356- i = close + 2
1530+ const end = scanBlockComment ( content , i )
1531+ if ( end === null ) return null
1532+ blank ( i , end )
1533+ i = end
13571534 continue
13581535 }
13591536
1360- if ( char === '/' && ( prevSignificant === '' || REGEX_ALLOWED_AFTER . has ( prevSignificant ) ) ) {
1361- let j = i + 1
1362- let inClass = false
1363- let closed = false
1364- while ( j < content . length ) {
1365- const c = content [ j ]
1366- if ( c === '\\' ) {
1367- j += 2
1368- continue
1369- }
1370- if ( c === '\n' ) break
1371- if ( c === '[' ) inClass = true
1372- else if ( c === ']' ) inClass = false
1373- else if ( c === '/' && ! inClass ) {
1374- closed = true
1375- break
1376- }
1377- j ++
1378- }
1379- if ( ! closed ) return null
1380- j ++
1381- while ( j < content . length && / [ a - z ] / . test ( content [ j ] ) ) j ++
1382- blank ( i , j )
1537+ if ( char === '/' && startsRegexLiteral ( content , i , prevSignificant ) ) {
1538+ const end = scanRegexLiteral ( content , i )
1539+ if ( end === null ) return null
1540+ blank ( i , end )
13831541 prevSignificant = ')'
1384- i = j
1542+ i = end
13851543 continue
13861544 }
13871545
13881546 if ( char === "'" || char === '"' ) {
1389- let j = i + 1
1390- let closed = false
1391- while ( j < content . length ) {
1392- if ( content [ j ] === '\\' ) {
1393- j += 2
1394- continue
1395- }
1396- if ( content [ j ] === '\n' ) break
1397- if ( content [ j ] === char ) {
1398- closed = true
1399- break
1400- }
1401- j ++
1402- }
1403- if ( ! closed ) return null
1404- blank ( i + 1 , j )
1547+ const close = scanQuoted ( content , i )
1548+ if ( close === null ) return null
1549+ blank ( i + 1 , close )
14051550 prevSignificant = char
1406- i = j + 1
1551+ i = close + 1
14071552 continue
14081553 }
14091554
14101555 if ( char === '`' ) {
1411- let j = i + 1
1412- let depth = 0
1413- let closed = false
1414- while ( j < content . length ) {
1415- if ( content [ j ] === '\\' ) {
1416- j += 2
1417- continue
1418- }
1419- if ( depth === 0 && content [ j ] === '`' ) {
1420- closed = true
1421- break
1422- }
1423- if ( content [ j ] === '$' && content [ j + 1 ] === '{' ) {
1424- depth ++
1425- j += 2
1426- continue
1427- }
1428- if ( depth > 0 && content [ j ] === '{' ) depth ++
1429- else if ( depth > 0 && content [ j ] === '}' ) depth --
1430- j ++
1431- }
1432- if ( ! closed ) return null
1433- blank ( i + 1 , j )
1556+ const close = scanTemplateLiteral ( content , i )
1557+ if ( close === null ) return null
1558+ blank ( i + 1 , close )
14341559 prevSignificant = '`'
1435- i = j + 1
1560+ i = close + 1
14361561 continue
14371562 }
14381563
0 commit comments