Skip to content

Commit 3a36a0f

Browse files
committed
.
1 parent 48680cc commit 3a36a0f

File tree

1 file changed

+117
-138
lines changed

1 file changed

+117
-138
lines changed

compiler/src/dotty/tools/dotc/parsing/Scanners.scala

Lines changed: 117 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ object Scanners {
388388
case InString(multiLine, _) if lastToken != STRINGPART => fetchStringPart(multiLine)
389389
case InDedentedString(quoteCount, _) if lastToken != STRINGPART =>
390390
offset = charOffset - 1
391-
getDedentedStringPartWithDelimiter(quoteCount)
391+
getDedentedStringPartWithDelimiter(quoteCount, isInterpolated = true)
392392
case _ => fetchToken()
393393
if token == ERROR then adjustSepRegions(STRINGLIT) // make sure we exit enclosing string literal
394394
else
@@ -1323,145 +1323,29 @@ object Scanners {
13231323
if (ch == CR) nextRawChar()
13241324
if (ch == LF) nextRawChar()
13251325

1326-
// For interpolated strings, check if we need to handle $ interpolation first
1327-
if (isInterpolated) {
1328-
getDedentedStringPartWithDelimiter(quoteCount)
1329-
quoteCount
1330-
} else {
1331-
// Collect all lines until we find the closing delimiter
1332-
val lines = scala.collection.mutable.ArrayBuffer[String]()
1333-
val lineIndents = scala.collection.mutable.ArrayBuffer[String]()
1334-
var currentLine = new StringBuilder
1335-
var currentIndent = new StringBuilder
1336-
var atLineStart = true
1337-
var closingIndent: String = null
1338-
var foundClosing = false
1339-
1340-
while (!foundClosing && ch != SU) {
1341-
if (atLineStart) {
1342-
// Collect indentation
1343-
currentIndent.clear()
1344-
while (ch == ' ' || ch == '\t') {
1345-
currentIndent.append(ch)
1346-
nextRawChar()
1347-
}
1348-
1349-
// Check if this might be the closing delimiter
1350-
if (ch == '\'') {
1351-
var endQuoteCount = 0
1352-
while (ch == '\'' && endQuoteCount < quoteCount + 1) {
1353-
endQuoteCount += 1
1354-
nextRawChar()
1355-
}
1356-
1357-
if (endQuoteCount == quoteCount && ch != '\'') {
1358-
// Found closing delimiter (not followed by another quote)
1359-
foundClosing = true
1360-
closingIndent = currentIndent.toString
1361-
} else {
1362-
// False alarm, these quotes are part of the content
1363-
// We need to restore and add them to current line
1364-
currentLine.append(currentIndent)
1365-
for (_ <- 0 until endQuoteCount) currentLine.append('\'')
1366-
atLineStart = false
1367-
}
1368-
} else {
1369-
atLineStart = false
1370-
}
1371-
}
1372-
1373-
if (!foundClosing && !atLineStart) {
1374-
// Regular content
1375-
if (ch == CR || ch == LF) {
1376-
// End of line
1377-
lineIndents += currentIndent.toString
1378-
lines += currentLine.toString
1379-
currentLine.clear()
1380-
currentIndent.clear()
1381-
1382-
// Normalize newlines to \n
1383-
if (ch == CR) nextRawChar()
1384-
if (ch == LF) nextRawChar()
1385-
atLineStart = true
1386-
} else {
1387-
currentLine.append(ch)
1388-
nextRawChar()
1389-
}
1390-
}
1391-
}
1392-
1393-
if (!foundClosing) {
1394-
incompleteInputError(em"unclosed dedented string literal")
1395-
} else if (closingIndent == null) {
1396-
error(em"internal error: closing indent not set")
1397-
token = ERROR
1398-
} else {
1399-
// Validate and dedent all lines
1400-
val dedentedLines = scala.collection.mutable.ArrayBuffer[String]()
1401-
val closingIndentLen = closingIndent.length
1402-
var hasSpaces = false
1403-
var hasTabs = false
1404-
1405-
for (indent <- closingIndent) {
1406-
if (indent == ' ') hasSpaces = true
1407-
if (indent == '\t') hasTabs = true
1408-
}
1326+
// Collect all content using the string part parser
1327+
getDedentedStringPartWithDelimiter(quoteCount, isInterpolated)
14091328

1410-
var hasError = false
1411-
for (i <- 0 until lines.length if !hasError) {
1412-
val line = lines(i)
1413-
val indent = lineIndents(i)
1414-
1415-
// Check for mixed tabs and spaces
1416-
var lineHasSpaces = false
1417-
var lineHasTabs = false
1418-
for (ch <- indent) {
1419-
if (ch == ' ') lineHasSpaces = true
1420-
if (ch == '\t') lineHasTabs = true
1421-
}
1422-
1423-
if ((hasSpaces && lineHasTabs) || (hasTabs && lineHasSpaces)) {
1424-
error(em"dedented string literal cannot mix tabs and spaces in indentation")
1425-
token = ERROR
1426-
hasError = true
1427-
} else if (line.isEmpty) {
1428-
// Empty lines are allowed
1429-
dedentedLines += ""
1430-
} else {
1431-
// Non-empty lines must be indented at least as much as closing delimiter
1432-
if (!indent.startsWith(closingIndent)) {
1433-
error(em"line in dedented string literal must be indented at least as much as the closing delimiter")
1434-
token = ERROR
1435-
hasError = true
1436-
} else {
1437-
// Remove the closing indentation from this line
1438-
dedentedLines += indent.substring(closingIndentLen) + line
1439-
}
1440-
}
1441-
}
1442-
1443-
if (!hasError) {
1444-
// Set the string value (join with \n)
1445-
strVal = dedentedLines.mkString("\n")
1446-
litBuf.clear()
1447-
token = STRINGLIT
1448-
}
1449-
}
1450-
1451-
quoteCount
1329+
// For non-interpolated strings, we need to dedent the collected content
1330+
if (!isInterpolated && token == STRINGLIT) {
1331+
dedentCollectedString()
14521332
}
1333+
1334+
quoteCount
14531335
}
14541336

1455-
/** Parse interpolated dedented string content, handling $ expressions.
1456-
* This collects content until hitting $ or closing delimiter.
1337+
/** Parse dedented string content, with optional $ interpolation handling.
1338+
* This collects content until hitting $ (if interpolated) or closing delimiter.
14571339
* Respects the quote count for extended delimiters.
14581340
*
1459-
* Note: This parses with the same format requirements as non-interpolated dedented strings
1460-
* (newline after opening, extended delimiters, etc.) but does NOT dedent the content during
1461-
* parsing. Dedenting for interpolated strings must be handled at runtime after all parts
1462-
* are assembled, similar to how the string interpolator combines the parts.
1341+
* @param quoteCount The number of quotes in the delimiter (3 for ''', 4 for '''', etc.)
1342+
* @param isInterpolated If true, handles $ expressions and returns STRINGPART tokens.
1343+
* If false, treats $ as regular content and returns STRINGLIT.
1344+
*
1345+
* Note: Interpolated strings do NOT dedent during parsing - dedenting must be handled
1346+
* at runtime after all parts are assembled. Non-interpolated strings dedent after collection.
14631347
*/
1464-
@tailrec private def getDedentedStringPartWithDelimiter(quoteCount: Int): Unit =
1348+
@tailrec private def getDedentedStringPartWithDelimiter(quoteCount: Int, isInterpolated: Boolean): Unit =
14651349
// Check for closing delimiter with correct quote count
14661350
if (ch == '\'') {
14671351
// Count the quotes we encounter
@@ -1479,10 +1363,10 @@ object Scanners {
14791363
} else {
14801364
// Not the closing delimiter, add the quotes we found to content
14811365
for (_ <- 0 until foundQuotes) putChar('\'')
1482-
getDedentedStringPartWithDelimiter(quoteCount)
1366+
getDedentedStringPartWithDelimiter(quoteCount, isInterpolated)
14831367
}
14841368
}
1485-
else if (ch == '$') {
1369+
else if (isInterpolated && ch == '$') {
14861370
// Handle interpolation
14871371
def getInterpolatedIdentRest(hasSupplement: Boolean): Unit =
14881372
@tailrec def loopRest(): Unit =
@@ -1510,7 +1394,7 @@ object Scanners {
15101394
if (ch == '$' || ch == '\'') {
15111395
putChar(ch)
15121396
nextRawChar()
1513-
getDedentedStringPartWithDelimiter(quoteCount)
1397+
getDedentedStringPartWithDelimiter(quoteCount, isInterpolated)
15141398
}
15151399
else if (ch == '{') {
15161400
setStrVal()
@@ -1523,7 +1407,7 @@ object Scanners {
15231407
else
15241408
error("invalid string interpolation: `$$`, `$'`, `$`ident or `$`BlockExpr expected".toMessage, off = charOffset - 2)
15251409
putChar('$')
1526-
getDedentedStringPartWithDelimiter(quoteCount)
1410+
getDedentedStringPartWithDelimiter(quoteCount, isInterpolated)
15271411
}
15281412
else {
15291413
val isUnclosedLiteral = !isUnicodeEscape && ch == SU
@@ -1532,11 +1416,106 @@ object Scanners {
15321416
else {
15331417
putChar(ch)
15341418
nextRawChar()
1535-
getDedentedStringPartWithDelimiter(quoteCount)
1419+
getDedentedStringPartWithDelimiter(quoteCount, isInterpolated)
15361420
}
15371421
}
15381422
end getDedentedStringPartWithDelimiter
15391423

1424+
/** Dedent a collected string by analyzing line structure and removing common indentation.
1425+
* This processes the content in `strVal`, validating indentation rules and removing
1426+
* the minimum common indentation from all non-empty lines.
1427+
*/
1428+
private def dedentCollectedString(): Unit = {
1429+
val content = strVal
1430+
if (content.isEmpty) return
1431+
1432+
val lines = scala.collection.mutable.ArrayBuffer[String]()
1433+
val lineIndents = scala.collection.mutable.ArrayBuffer[String]()
1434+
1435+
// Parse content into lines with their indentation
1436+
var i = 0
1437+
while (i < content.length) {
1438+
// Collect indentation for this line
1439+
val indentStart = i
1440+
while (i < content.length && (content(i) == ' ' || content(i) == '\t')) {
1441+
i += 1
1442+
}
1443+
val indent = content.substring(indentStart, i)
1444+
1445+
// Collect rest of line
1446+
val lineStart = i
1447+
while (i < content.length && content(i) != '\n') {
1448+
i += 1
1449+
}
1450+
val line = content.substring(lineStart, i)
1451+
1452+
lines += line
1453+
lineIndents += indent
1454+
1455+
// Skip the newline
1456+
if (i < content.length && content(i) == '\n') {
1457+
i += 1
1458+
}
1459+
}
1460+
1461+
// The last line's indentation is the closing indentation
1462+
if (lines.isEmpty) {
1463+
strVal = ""
1464+
return
1465+
}
1466+
1467+
val closingIndent = lineIndents.last
1468+
val closingIndentLen = closingIndent.length
1469+
1470+
// Check for mixed tabs/spaces in closing indent
1471+
var hasSpaces = false
1472+
var hasTabs = false
1473+
for (ch <- closingIndent) {
1474+
if (ch == ' ') hasSpaces = true
1475+
if (ch == '\t') hasTabs = true
1476+
}
1477+
1478+
// Validate and dedent all lines
1479+
val dedentedLines = scala.collection.mutable.ArrayBuffer[String]()
1480+
var hasError = false
1481+
1482+
for (i <- 0 until lines.length - 1 if !hasError) { // Skip last line (it's empty after closing delimiter)
1483+
val line = lines(i)
1484+
val indent = lineIndents(i)
1485+
1486+
// Check for mixed tabs and spaces
1487+
var lineHasSpaces = false
1488+
var lineHasTabs = false
1489+
for (ch <- indent) {
1490+
if (ch == ' ') lineHasSpaces = true
1491+
if (ch == '\t') lineHasTabs = true
1492+
}
1493+
1494+
if ((hasSpaces && lineHasTabs) || (hasTabs && lineHasSpaces)) {
1495+
error(em"dedented string literal cannot mix tabs and spaces in indentation")
1496+
token = ERROR
1497+
hasError = true
1498+
} else if (line.isEmpty) {
1499+
// Empty lines are allowed
1500+
dedentedLines += ""
1501+
} else {
1502+
// Non-empty lines must be indented at least as much as closing delimiter
1503+
if (!indent.startsWith(closingIndent)) {
1504+
error(em"line in dedented string literal must be indented at least as much as the closing delimiter")
1505+
token = ERROR
1506+
hasError = true
1507+
} else {
1508+
// Remove the closing indentation from this line
1509+
dedentedLines += indent.substring(closingIndentLen) + line
1510+
}
1511+
}
1512+
}
1513+
1514+
if (!hasError) {
1515+
strVal = dedentedLines.mkString("\n")
1516+
}
1517+
}
1518+
15401519
private def getRawStringLit(): Unit =
15411520
if (ch == '\"') {
15421521
nextRawChar()

0 commit comments

Comments
 (0)