Skip to content

Commit 40f397f

Browse files
committed
.
1 parent 00f04b8 commit 40f397f

File tree

1 file changed

+152
-150
lines changed

1 file changed

+152
-150
lines changed

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

Lines changed: 152 additions & 150 deletions
Original file line numberDiff line numberDiff line change
@@ -986,10 +986,6 @@ object Scanners {
986986
}
987987
fetchDoubleQuote()
988988
case '\'' =>
989-
def dedentedStringPart() = {
990-
getDedentedString(isInterpolated = true)
991-
currentRegion = InDedentedString(currentRegion)
992-
}
993989
def fetchSingleQuote(): Unit = {
994990
nextChar()
995991
// Check for triple single quote (dedented string literal)
@@ -1001,7 +997,8 @@ object Scanners {
1001997
if (token == INTERPOLATIONID) {
1002998
// For interpolation, handle as string part
1003999
nextRawChar()
1004-
dedentedStringPart()
1000+
getDedentedString(isInterpolated = true)
1001+
currentRegion = InDedentedString(currentRegion)
10051002
} else {
10061003
getDedentedString(isInterpolated = false)
10071004
}
@@ -1300,177 +1297,182 @@ object Scanners {
13001297
* @param isInterpolated If true, handles $ interpolation and returns STRINGPART tokens
13011298
*/
13021299
private def getDedentedString(isInterpolated: Boolean): Unit = {
1300+
// For interpolated strings, we're already at the first character after '''
1301+
// For non-interpolated, we need to consume the first character
1302+
if (!isInterpolated) nextChar()
1303+
1304+
// Count opening quotes (already consumed 3)
1305+
var quoteCount = 3
1306+
while (ch == '\'') {
1307+
quoteCount += 1
1308+
if (isInterpolated) nextRawChar() else nextChar()
1309+
}
1310+
1311+
// Must be followed by a newline
1312+
if (ch != LF && ch != CR) {
1313+
error(em"dedented string literal must start with newline after opening quotes")
1314+
token = ERROR
1315+
return
1316+
}
1317+
1318+
// Skip the initial newline (CR LF or just LF)
1319+
if (ch == CR) nextRawChar()
1320+
if (ch == LF) nextRawChar()
1321+
1322+
// For interpolated strings, check if we need to handle $ interpolation first
13031323
if (isInterpolated) {
1304-
// For interpolated strings: parse incrementally, handling $ expressions
1305-
getDedentedStringPartImpl()
1324+
getDedentedStringPartWithDelimiter(quoteCount)
13061325
} else {
1307-
// For non-interpolated strings: parse entire string and dedent
1308-
// Count opening quotes (already consumed 3)
1309-
nextChar()
1310-
var quoteCount = 3
1311-
while (ch == '\'') {
1312-
quoteCount += 1
1313-
nextChar()
1314-
}
1326+
// Collect all lines until we find the closing delimiter
1327+
val lines = scala.collection.mutable.ArrayBuffer[String]()
1328+
val lineIndents = scala.collection.mutable.ArrayBuffer[String]()
1329+
var currentLine = new StringBuilder
1330+
var currentIndent = new StringBuilder
1331+
var atLineStart = true
1332+
var closingIndent: String = null
1333+
var foundClosing = false
1334+
1335+
while (!foundClosing && ch != SU) {
1336+
if (atLineStart) {
1337+
// Collect indentation
1338+
currentIndent.clear()
1339+
while (ch == ' ' || ch == '\t') {
1340+
currentIndent.append(ch)
1341+
nextRawChar()
1342+
}
13151343

1316-
// Must be followed by a newline
1317-
if (ch != LF && ch != CR) {
1318-
error(em"dedented string literal must start with newline after opening quotes")
1319-
token = ERROR
1320-
} else {
1321-
// Skip the initial newline (CR LF or just LF)
1322-
if (ch == CR) nextRawChar()
1323-
if (ch == LF) nextRawChar()
1324-
1325-
// Collect all lines until we find the closing delimiter
1326-
val lines = scala.collection.mutable.ArrayBuffer[String]()
1327-
val lineIndents = scala.collection.mutable.ArrayBuffer[String]()
1328-
var currentLine = new StringBuilder
1329-
var currentIndent = new StringBuilder
1330-
var atLineStart = true
1331-
var closingIndent: String = null
1332-
var foundClosing = false
1333-
1334-
while (!foundClosing && ch != SU) {
1335-
if (atLineStart) {
1336-
// Collect indentation
1337-
currentIndent.clear()
1338-
while (ch == ' ' || ch == '\t') {
1339-
currentIndent.append(ch)
1344+
// Check if this might be the closing delimiter
1345+
if (ch == '\'') {
1346+
var endQuoteCount = 0
1347+
while (ch == '\'' && endQuoteCount < quoteCount + 1) {
1348+
endQuoteCount += 1
13401349
nextRawChar()
13411350
}
13421351

1343-
// Check if this might be the closing delimiter
1344-
if (ch == '\'') {
1345-
var endQuoteCount = 0
1346-
while (ch == '\'' && endQuoteCount < quoteCount + 1) {
1347-
endQuoteCount += 1
1348-
nextRawChar()
1349-
}
1350-
1351-
if (endQuoteCount == quoteCount && ch != '\'') {
1352-
// Found closing delimiter (not followed by another quote)
1353-
foundClosing = true
1354-
closingIndent = currentIndent.toString
1355-
} else {
1356-
// False alarm, these quotes are part of the content
1357-
// We need to restore and add them to current line
1358-
currentLine.append(currentIndent)
1359-
for (_ <- 0 until endQuoteCount) currentLine.append('\'')
1360-
atLineStart = false
1361-
}
1352+
if (endQuoteCount == quoteCount && ch != '\'') {
1353+
// Found closing delimiter (not followed by another quote)
1354+
foundClosing = true
1355+
closingIndent = currentIndent.toString
13621356
} else {
1357+
// False alarm, these quotes are part of the content
1358+
// We need to restore and add them to current line
1359+
currentLine.append(currentIndent)
1360+
for (_ <- 0 until endQuoteCount) currentLine.append('\'')
13631361
atLineStart = false
13641362
}
1363+
} else {
1364+
atLineStart = false
13651365
}
1366+
}
13661367

1367-
if (!foundClosing && !atLineStart) {
1368-
// Regular content
1369-
if (ch == CR || ch == LF) {
1370-
// End of line
1371-
lineIndents += currentIndent.toString
1372-
lines += currentLine.toString
1373-
currentLine.clear()
1374-
currentIndent.clear()
1375-
1376-
// Normalize newlines to \n
1377-
if (ch == CR) nextRawChar()
1378-
if (ch == LF) nextRawChar()
1379-
atLineStart = true
1380-
} else {
1381-
currentLine.append(ch)
1382-
nextRawChar()
1383-
}
1368+
if (!foundClosing && !atLineStart) {
1369+
// Regular content
1370+
if (ch == CR || ch == LF) {
1371+
// End of line
1372+
lineIndents += currentIndent.toString
1373+
lines += currentLine.toString
1374+
currentLine.clear()
1375+
currentIndent.clear()
1376+
1377+
// Normalize newlines to \n
1378+
if (ch == CR) nextRawChar()
1379+
if (ch == LF) nextRawChar()
1380+
atLineStart = true
1381+
} else {
1382+
currentLine.append(ch)
1383+
nextRawChar()
13841384
}
13851385
}
1386+
}
13861387

1387-
if (!foundClosing) {
1388-
incompleteInputError(em"unclosed dedented string literal")
1389-
} else if (closingIndent == null) {
1390-
error(em"internal error: closing indent not set")
1391-
token = ERROR
1392-
} else {
1393-
// Validate and dedent all lines
1394-
val dedentedLines = scala.collection.mutable.ArrayBuffer[String]()
1395-
val closingIndentLen = closingIndent.length
1396-
var hasSpaces = false
1397-
var hasTabs = false
1398-
1399-
for (indent <- closingIndent) {
1400-
if (indent == ' ') hasSpaces = true
1401-
if (indent == '\t') hasTabs = true
1402-
}
1388+
if (!foundClosing) {
1389+
incompleteInputError(em"unclosed dedented string literal")
1390+
} else if (closingIndent == null) {
1391+
error(em"internal error: closing indent not set")
1392+
token = ERROR
1393+
} else {
1394+
// Validate and dedent all lines
1395+
val dedentedLines = scala.collection.mutable.ArrayBuffer[String]()
1396+
val closingIndentLen = closingIndent.length
1397+
var hasSpaces = false
1398+
var hasTabs = false
1399+
1400+
for (indent <- closingIndent) {
1401+
if (indent == ' ') hasSpaces = true
1402+
if (indent == '\t') hasTabs = true
1403+
}
14031404

1404-
var hasError = false
1405-
for (i <- 0 until lines.length if !hasError) {
1406-
val line = lines(i)
1407-
val indent = lineIndents(i)
1408-
1409-
// Check for mixed tabs and spaces
1410-
var lineHasSpaces = false
1411-
var lineHasTabs = false
1412-
for (ch <- indent) {
1413-
if (ch == ' ') lineHasSpaces = true
1414-
if (ch == '\t') lineHasTabs = true
1415-
}
1405+
var hasError = false
1406+
for (i <- 0 until lines.length if !hasError) {
1407+
val line = lines(i)
1408+
val indent = lineIndents(i)
1409+
1410+
// Check for mixed tabs and spaces
1411+
var lineHasSpaces = false
1412+
var lineHasTabs = false
1413+
for (ch <- indent) {
1414+
if (ch == ' ') lineHasSpaces = true
1415+
if (ch == '\t') lineHasTabs = true
1416+
}
14161417

1417-
if ((hasSpaces && lineHasTabs) || (hasTabs && lineHasSpaces)) {
1418-
error(em"dedented string literal cannot mix tabs and spaces in indentation")
1418+
if ((hasSpaces && lineHasTabs) || (hasTabs && lineHasSpaces)) {
1419+
error(em"dedented string literal cannot mix tabs and spaces in indentation")
1420+
token = ERROR
1421+
hasError = true
1422+
} else if (line.isEmpty) {
1423+
// Empty lines are allowed
1424+
dedentedLines += ""
1425+
} else {
1426+
// Non-empty lines must be indented at least as much as closing delimiter
1427+
if (!indent.startsWith(closingIndent)) {
1428+
error(em"line in dedented string literal must be indented at least as much as the closing delimiter")
14191429
token = ERROR
14201430
hasError = true
1421-
} else if (line.isEmpty) {
1422-
// Empty lines are allowed
1423-
dedentedLines += ""
14241431
} else {
1425-
// Non-empty lines must be indented at least as much as closing delimiter
1426-
if (!indent.startsWith(closingIndent)) {
1427-
error(em"line in dedented string literal must be indented at least as much as the closing delimiter")
1428-
token = ERROR
1429-
hasError = true
1430-
} else {
1431-
// Remove the closing indentation from this line
1432-
dedentedLines += indent.substring(closingIndentLen) + line
1433-
}
1432+
// Remove the closing indentation from this line
1433+
dedentedLines += indent.substring(closingIndentLen) + line
14341434
}
14351435
}
1436+
}
14361437

1437-
if (!hasError) {
1438-
// Set the string value (join with \n)
1439-
strVal = dedentedLines.mkString("\n")
1440-
litBuf.clear()
1441-
token = STRINGLIT
1442-
}
1438+
if (!hasError) {
1439+
// Set the string value (join with \n)
1440+
strVal = dedentedLines.mkString("\n")
1441+
litBuf.clear()
1442+
token = STRINGLIT
14431443
}
14441444
}
14451445
}
14461446
}
14471447

1448-
/** For interpolated dedented strings - parse string content until ''' or $ */
1449-
@tailrec private def getDedentedStringPartImpl(): Unit =
1450-
// Check for closing ''' delimiter
1448+
/** Parse interpolated dedented string content, handling $ expressions.
1449+
* This collects content until hitting $ or closing delimiter.
1450+
* Respects the quote count for extended delimiters.
1451+
*
1452+
* Note: This parses with the same format requirements as non-interpolated dedented strings
1453+
* (newline after opening, extended delimiters, etc.) but does NOT dedent the content during
1454+
* parsing. Dedenting for interpolated strings must be handled at runtime after all parts
1455+
* are assembled, similar to how the string interpolator combines the parts.
1456+
*/
1457+
@tailrec private def getDedentedStringPartWithDelimiter(quoteCount: Int): Unit =
1458+
// Check for closing delimiter with correct quote count
14511459
if (ch == '\'') {
1452-
nextRawChar()
1453-
if (ch == '\'') {
1460+
// Count the quotes we encounter
1461+
var foundQuotes = 0
1462+
while (ch == '\'' && foundQuotes < quoteCount + 1) {
1463+
foundQuotes += 1
14541464
nextRawChar()
1455-
if (ch == '\'') {
1456-
// Found closing '''
1457-
nextChar()
1458-
// For now, set the string value without dedenting
1459-
// TODO: implement proper dedenting for interpolated strings
1460-
setStrVal()
1461-
token = STRINGLIT
1462-
}
1463-
else {
1464-
// Two quotes followed by something else, add them to content
1465-
putChar('\'')
1466-
putChar('\'')
1467-
getDedentedStringPartImpl()
1468-
}
14691465
}
1470-
else {
1471-
// Single quote followed by something else, add it to content
1472-
putChar('\'')
1473-
getDedentedStringPartImpl()
1466+
1467+
if (foundQuotes == quoteCount && ch != '\'') {
1468+
// Found closing delimiter - exact match and not followed by another quote
1469+
nextChar()
1470+
setStrVal()
1471+
token = STRINGLIT
1472+
} else {
1473+
// Not the closing delimiter, add the quotes we found to content
1474+
for (_ <- 0 until foundQuotes) putChar('\'')
1475+
getDedentedStringPartWithDelimiter(quoteCount)
14741476
}
14751477
}
14761478
else if (ch == '$') {
@@ -1501,7 +1503,7 @@ object Scanners {
15011503
if (ch == '$' || ch == '\'') {
15021504
putChar(ch)
15031505
nextRawChar()
1504-
getDedentedStringPartImpl()
1506+
getDedentedStringPartWithDelimiter(quoteCount)
15051507
}
15061508
else if (ch == '{') {
15071509
setStrVal()
@@ -1514,7 +1516,7 @@ object Scanners {
15141516
else
15151517
error("invalid string interpolation: `$$`, `$'`, `$`ident or `$`BlockExpr expected".toMessage, off = charOffset - 2)
15161518
putChar('$')
1517-
getDedentedStringPartImpl()
1519+
getDedentedStringPartWithDelimiter(quoteCount)
15181520
}
15191521
else {
15201522
val isUnclosedLiteral = !isUnicodeEscape && ch == SU
@@ -1523,10 +1525,10 @@ object Scanners {
15231525
else {
15241526
putChar(ch)
15251527
nextRawChar()
1526-
getDedentedStringPartImpl()
1528+
getDedentedStringPartWithDelimiter(quoteCount)
15271529
}
15281530
}
1529-
end getDedentedStringPartImpl
1531+
end getDedentedStringPartWithDelimiter
15301532

15311533
private def getRawStringLit(): Unit =
15321534
if (ch == '\"') {

0 commit comments

Comments
 (0)