@@ -516,10 +516,97 @@ public Label visit(Literal nd, Context c) {
516516 String valueString = nd .getStringValue ();
517517
518518 trapwriter .addTuple ("literals" , valueString , source , key );
519- if (nd .isRegExp ()) regexpExtractor .extract (source .substring (1 , source .lastIndexOf ('/' )), nd );
519+ if (nd .isRegExp ()) {
520+ OffsetTranslation offsets = new OffsetTranslation ();
521+ offsets .set (0 , 1 ); // skip the initial '/'
522+ regexpExtractor .extract (source .substring (1 , source .lastIndexOf ('/' )), offsets , nd , false );
523+ } else if (nd .isStringLiteral () && !c .isInsideType ()) {
524+ regexpExtractor .extract (valueString , makeStringLiteralOffsets (nd .getRaw ()), nd , true );
525+ }
520526 return key ;
521527 }
522528
529+ private boolean isOctalDigit (char ch ) {
530+ return '0' <= ch && ch <= '7' ;
531+ }
532+
533+ /**
534+ * Builds a translation from offsets in a string value back to its original raw literal text
535+ * (including quotes).
536+ *
537+ * <p>This is not a 1:1 mapping since escape sequences take up more characters in the raw
538+ * literal than in the resulting string value. This mapping includes the surrounding quotes.
539+ *
540+ * <p>For example: for the raw literal value <code>'x\.y'</code> (quotes included), the <code>y
541+ * </code> at index 2 in <code>x.y</code> maps to index 4 in the raw literal.
542+ */
543+ public OffsetTranslation makeStringLiteralOffsets (String rawLiteral ) {
544+ OffsetTranslation offsets = new OffsetTranslation ();
545+ offsets .set (0 , 1 ); // Skip the initial quote
546+ // Invariant: raw character at 'pos' corresponds to decoded character at 'pos - delta'
547+ int pos = 1 ;
548+ int delta = 1 ;
549+ while (pos < rawLiteral .length () - 1 ) {
550+ if (rawLiteral .charAt (pos ) != '\\' ) {
551+ ++pos ;
552+ continue ;
553+ }
554+ final int length ; // Length of the escape sequence, including slash.
555+ int outputLength = 1 ; // Number characters the sequence expands to.
556+ char ch = rawLiteral .charAt (pos + 1 );
557+ if ('0' <= ch && ch <= '7' ) {
558+ // Octal escape: \N, \NN, or \NNN
559+ int firstDigit = pos + 1 ;
560+ int end = firstDigit ;
561+ int maxEnd = Math .min (firstDigit + (ch <= '3' ? 3 : 2 ), rawLiteral .length ());
562+ while (end < maxEnd && isOctalDigit (rawLiteral .charAt (end ))) {
563+ ++end ;
564+ }
565+ length = end - pos ;
566+ } else if (ch == 'x' ) {
567+ // Hex escape: \xNN
568+ length = 4 ;
569+ } else if (ch == 'u' && pos + 2 < rawLiteral .length ()) {
570+ if (rawLiteral .charAt (pos + 2 ) == '{' ) {
571+ // Variable-length unicode escape: \U{N...}
572+ // Scan for the ending '}'
573+ int firstDigit = pos + 3 ;
574+ int end = firstDigit ;
575+ int leadingZeros = 0 ;
576+ while (end < rawLiteral .length () && rawLiteral .charAt (end ) == '0' ) {
577+ ++end ;
578+ ++leadingZeros ;
579+ }
580+ while (end < rawLiteral .length () && rawLiteral .charAt (end ) != '}' ) {
581+ ++end ;
582+ }
583+ int numDigits = end - firstDigit ;
584+ if (numDigits - leadingZeros > 4 ) {
585+ outputLength = 2 ; // Encoded as a surrogate pair
586+ }
587+ ++end ; // Include '}' character
588+ length = end - pos ;
589+ } else {
590+ // Fixed-length unicode escape: \UNNNN
591+ length = 6 ;
592+ }
593+ } else {
594+ // Simple escape: \n or similar.
595+ length = 2 ;
596+ }
597+ int end = pos + length ;
598+ if (end > rawLiteral .length ()) {
599+ end = rawLiteral .length ();
600+ }
601+ int outputPos = pos - delta ;
602+ // Map the next character to the adjusted offset.
603+ offsets .set (outputPos + outputLength , end );
604+ delta += length - outputLength ;
605+ pos = end ;
606+ }
607+ return offsets ;
608+ }
609+
523610 @ Override
524611 public Label visit (MemberExpression nd , Context c ) {
525612 Label key = super .visit (nd , c );
0 commit comments