@@ -1368,26 +1368,43 @@ object Parsers {
13681368 // APIs behaves predictably in the presence of empty leading/trailing lines
13691369 if (closingIndent == " " ) str
13701370 else {
1371- if (closingIndent.contains('\t ' ) && closingIndent.contains(' ' )) {
1371+ // Check for mixed tabs and spaces in closing indent
1372+
1373+ val hasTabs = closingIndent.contains('\t ' )
1374+ val hasSpaces = closingIndent.contains(' ' )
1375+ if (hasTabs && hasSpaces) {
13721376 syntaxError(
13731377 em " dedented string literal cannot mix tabs and spaces in indentation " ,
13741378 offset
13751379 )
13761380 return str
13771381 }
13781382
1383+ // Split into lines
13791384 val linesAndWithSeps = (str.linesIterator.zip(str.linesWithSeparators)).toSeq
1385+
13801386 var lineOffset = offset
13811387
13821388 def dedentLine (line : String , lineWithSep : String ) = {
13831389 val result =
13841390 if (line.startsWith(closingIndent)) line.substring(closingIndent.length)
13851391 else if (line.trim.isEmpty) " " // Empty or whitespace-only lines
13861392 else {
1387- syntaxError(
1388- em " line in dedented string literal must be indented at least as much as the closing delimiter with an identical prefix " ,
1389- lineOffset
1390- )
1393+ // Check if this line has mixed tabs/spaces that don't match closing indent
1394+ val lineIndent = line.takeWhile(_.isWhitespace)
1395+ val lineHasTabs = lineIndent.contains('\t ' )
1396+ val lineHasSpaces = lineIndent.contains(' ' )
1397+ if ((hasTabs && lineHasSpaces && ! lineHasTabs) || (hasSpaces && lineHasTabs && ! lineHasSpaces)) {
1398+ syntaxError(
1399+ em " dedented string literal cannot mix tabs and spaces in indentation " ,
1400+ offset
1401+ )
1402+ } else {
1403+ syntaxError(
1404+ em " line in dedented string literal must be indented at least as much as the closing delimiter " ,
1405+ lineOffset
1406+ )
1407+ }
13911408 line
13921409 }
13931410 lineOffset += lineWithSep.length // Make sure to include any \n, \r, \r\n, or \n\r
@@ -1534,15 +1551,14 @@ object Parsers {
15341551 in.charOffset + 1 < in.buf.length &&
15351552 in.buf(in.charOffset) == '"' &&
15361553 in.buf(in.charOffset + 1 ) == '"'
1537-
15381554 val isDedented =
15391555 in.charOffset + 2 < in.buf.length &&
15401556 in.buf(in.charOffset - 1 ) == '\' ' &&
15411557 in.buf(in.charOffset) == '\' ' &&
15421558 in.buf(in.charOffset + 1 ) == '\' '
1543-
15441559 in.nextToken()
15451560
1561+ // Collect all string parts and their offsets
15461562 val stringParts = new ListBuffer [(String , Offset )]
15471563 val interpolatedExprs = new ListBuffer [Tree ]
15481564
@@ -1553,6 +1569,7 @@ object Parsers {
15531569 offsetCorrection = 0
15541570 in.nextToken()
15551571
1572+ // Collect the interpolated expression
15561573 interpolatedExprs += atSpan(in.offset) {
15571574 if (in.token == IDENTIFIER )
15581575 termIdent()
@@ -1574,6 +1591,7 @@ object Parsers {
15741591 }
15751592 }
15761593
1594+ // Get the final STRINGLIT
15771595 val finalLiteral = if (in.token == STRINGLIT ) {
15781596 val s = in.strVal
15791597 val off = in.offset + offsetCorrection
@@ -1593,6 +1611,7 @@ object Parsers {
15931611 }
15941612 }
15951613
1614+ // Build the segments with dedented strings
15961615 for ((str, expr) <- dedentedParts.zip(interpolatedExprs)) {
15971616 val (dedentedStr, offset) = str
15981617 segmentBuf += Thicket (
@@ -1601,7 +1620,8 @@ object Parsers {
16011620 )
16021621 }
16031622
1604- if (finalLiteral) { // Add the final literal if present
1623+ // Add the final literal if present
1624+ if (finalLiteral) {
16051625 val (dedentedStr, offset) = dedentedParts.last
16061626 segmentBuf += atSpan(offset, offset, offset + dedentedStr.length) { Literal (Constant (dedentedStr)) }
16071627 }
0 commit comments