Skip to content

Commit b1613c7

Browse files
committed
.
1 parent 3ea3e7e commit b1613c7

File tree

4 files changed

+106
-109
lines changed

4 files changed

+106
-109
lines changed

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

Lines changed: 82 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1364,71 +1364,76 @@ object Parsers {
13641364
isFirstPart: Boolean,
13651365
isLastPart: Boolean): String = {
13661366

1367-
// Check for mixed tabs and spaces in closing indent
1368-
val hasTabs = closingIndent.contains('\t')
1369-
val hasSpaces = closingIndent.contains(' ')
1370-
if (hasTabs && hasSpaces) {
1371-
syntaxError(
1372-
em"dedented string literal cannot mix tabs and spaces in indentation",
1373-
offset
1374-
)
1375-
return str
1376-
}
1367+
if (closingIndent == "") str
1368+
else {
1369+
// Check for mixed tabs and spaces in closing indent
1370+
1371+
val hasTabs = closingIndent.contains('\t')
1372+
val hasSpaces = closingIndent.contains(' ')
1373+
if (hasTabs && hasSpaces) {
1374+
syntaxError(
1375+
em"dedented string literal cannot mix tabs and spaces in indentation",
1376+
offset
1377+
)
1378+
return str
1379+
}
13771380

1378-
// Split into lines
1379-
val linesAndWithSeps = (str.linesIterator.zip(str.linesWithSeparators)).toSeq
1381+
// Split into lines
1382+
val linesAndWithSeps = (str.linesIterator.zip(str.linesWithSeparators)).toSeq
13801383

1381-
var lineOffset = offset
1382-
def dedentLine(line: String, lineWithSep: String) = {
1383-
val result =
1384-
if (line.startsWith(closingIndent)) line.substring(closingIndent.length)
1385-
else if (line.trim.isEmpty) "" // Empty or whitespace-only lines
1386-
else {
1387-
// Check if this line has mixed tabs/spaces that don't match closing indent
1388-
val lineIndent = line.takeWhile(_.isWhitespace)
1389-
val lineHasTabs = lineIndent.contains('\t')
1390-
val lineHasSpaces = lineIndent.contains(' ')
1391-
if ((hasTabs && lineHasSpaces && !lineHasTabs) || (hasSpaces && lineHasTabs && !lineHasSpaces)) {
1392-
syntaxError(
1393-
em"dedented string literal cannot mix tabs and spaces in indentation",
1394-
offset
1395-
)
1396-
} else {
1397-
syntaxError(
1398-
em"line in dedented string literal must be indented at least as much as the closing delimiter",
1399-
lineOffset
1400-
)
1384+
var lineOffset = offset
1385+
1386+
def dedentLine(line: String, lineWithSep: String) = {
1387+
val result =
1388+
if (line.startsWith(closingIndent)) line.substring(closingIndent.length)
1389+
else if (line.trim.isEmpty) "" // Empty or whitespace-only lines
1390+
else {
1391+
// Check if this line has mixed tabs/spaces that don't match closing indent
1392+
val lineIndent = line.takeWhile(_.isWhitespace)
1393+
val lineHasTabs = lineIndent.contains('\t')
1394+
val lineHasSpaces = lineIndent.contains(' ')
1395+
if ((hasTabs && lineHasSpaces && !lineHasTabs) || (hasSpaces && lineHasTabs && !lineHasSpaces)) {
1396+
syntaxError(
1397+
em"dedented string literal cannot mix tabs and spaces in indentation",
1398+
offset
1399+
)
1400+
} else {
1401+
syntaxError(
1402+
em"line in dedented string literal must be indented at least as much as the closing delimiter",
1403+
lineOffset
1404+
)
1405+
}
1406+
line
14011407
}
1402-
line
1408+
lineOffset += lineWithSep.length // Make sure to include any \n, \r, \r\n, or \n\r
1409+
result
1410+
}
1411+
1412+
// If this is the first part of a string, then the first line is the empty string following
1413+
// the opening `'''` delimiter, so we skip it. If not, then the first line is immediately
1414+
// following an interpolated value, and should be used raw without indenting
1415+
val firstLine =
1416+
if (isFirstPart) Nil
1417+
else {
1418+
val (line, lineWithSep) = linesAndWithSeps.head
1419+
lineOffset += lineWithSep.length
1420+
Seq(line)
14031421
}
1404-
lineOffset += lineWithSep.length // Make sure to include any \n, \r, \r\n, or \n\r
1405-
result
1406-
}
14071422

1408-
// If this is the first part of a string, then the first line is the empty string following
1409-
// the opening `'''` delimiter, so we skip it. If not, then the first line is immediately
1410-
// following an interpolated value, and should be used raw without indenting
1411-
val firstLine =
1412-
if (isFirstPart) Nil
1413-
else {
1414-
val (line, lineWithSep) = linesAndWithSeps.head
1415-
lineOffset += lineWithSep.length
1416-
Seq(line)
1423+
// Process all lines except the first and last, which require special handling
1424+
val dedented = linesAndWithSeps.drop(1).dropRight(1).map { case (line, lineWithSep) =>
1425+
dedentLine(line, lineWithSep)
14171426
}
14181427

1419-
// Process all lines except the first and last, which require special handling
1420-
val dedented = linesAndWithSeps.drop(1).dropRight(1).map { case (line, lineWithSep) =>
1421-
dedentLine(line, lineWithSep)
1422-
}
1428+
// If this is the last part of the string, then the last line is the indentation-only
1429+
// line preceding the closing delimiter, and should be ignored. If not, then the last line
1430+
// also needs to be de-dented
1431+
val lastLine =
1432+
if (isLastPart) Nil
1433+
else Seq(dedentLine(linesAndWithSeps.last._1, linesAndWithSeps.last._2))
14231434

1424-
// If this is the last part of the string, then the last line is the indentation-only
1425-
// line preceding the closing delimiter, and should be ignored. If not, then the last line
1426-
// also needs to be de-dented
1427-
val lastLine =
1428-
if (isLastPart) Nil
1429-
else Seq(dedentLine(linesAndWithSeps.last._1, linesAndWithSeps.last._2))
1430-
1431-
(firstLine ++ dedented ++ lastLine).mkString("\n")
1435+
(firstLine ++ dedented ++ lastLine).mkString("\n")
1436+
}
14321437
}
14331438

14341439
/** Literal ::= SimpleLiteral
@@ -1597,21 +1602,22 @@ object Parsers {
15971602
} else false
15981603

15991604
val dedentedParts =
1600-
if (!isDedented) stringParts
1605+
if (!isDedented || stringParts.isEmpty) stringParts
16011606
else {
16021607
val lastPart = stringParts.last._1
16031608
val closingIndent = extractClosingIndent(lastPart, in.offset)
16041609
stringParts.zipWithIndex.map { case ((str, offset), index) =>
1605-
(dedentString(str, in.offset, closingIndent, index == 0, index == stringParts.length - 1), offset)
1610+
val dedented = dedentString(str, in.offset, closingIndent, index == 0, index == stringParts.length - 1)
1611+
(dedented, offset)
16061612
}
16071613
}
16081614

16091615
// Build the segments with dedented strings
1610-
for (i <- 0 until dedentedParts.size - 1) {
1611-
val (dedentedStr, offset) = dedentedParts(i)
1616+
for ((str, expr) <- dedentedParts.zip(interpolatedExprs)) {
1617+
val (dedentedStr, offset) = str
16121618
segmentBuf += Thicket(
16131619
atSpan(offset, offset, offset + dedentedStr.length) { Literal(Constant(dedentedStr)) },
1614-
interpolatedExprs(i)
1620+
expr
16151621
)
16161622
}
16171623

@@ -1626,14 +1632,26 @@ object Parsers {
16261632

16271633
/** Extract the closing indentation from the last line of a string */
16281634
private def extractClosingIndent(str: String, offset: Offset): String = {
1629-
val closingIndent = str.linesIterator.toSeq.last
1630-
if (!closingIndent.forall(_.isWhitespace)) {
1635+
// If the last line is empty, `linesIterator` and `linesWithSeparators` skips
1636+
// the empty string, so we must recognize that case and explicitly default to ""
1637+
// otherwise things will blow up
1638+
val closingIndent = str
1639+
.linesIterator
1640+
.zip(str.linesWithSeparators)
1641+
.toSeq
1642+
.lastOption
1643+
.filter((line, lineWithSep) => line == lineWithSep)
1644+
.map(_._1)
1645+
.getOrElse("")
1646+
1647+
if (closingIndent.exists(!_.isWhitespace)) {
16311648
syntaxError(
16321649
em"last line of dedented string literal must contain only whitespace before closing delimiter",
16331650
offset
16341651
)
16351652
return str
16361653
}
1654+
16371655
closingIndent
16381656
}
16391657

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
1-
-- Error: tests/neg/dedented-string-literals.scala:5:27
2-
5 | val noNewlineAfterOpen = '''content on same line // error
1+
-- Error: ----------------------------------------------------------------------
2+
5 | val noNewlineAfterOpen = '''content on same line // error: dedented string literal must start with a newline
33
| ^
4-
|dedented string literal must start with newline after opening quotes
5-
-- Error: tests/neg/dedented-string-literals.scala:8:20
4+
| dedented string literal must start with newline after opening quotes
5+
-- Error: ----------------------------------------------------------------------
66
8 | val notIndented = '''
77
| ^
88
|line in dedented string literal must be indented at least as much as the closing delimiter
9-
-- Error: tests/neg/dedented-string-literals.scala:13:24
9+
-- Error: ----------------------------------------------------------------------
1010
13 | val mixedTabsSpaces = '''
1111
| ^
12-
|dedented string literal cannot mix tabs and spaces in indentation
13-
-- Error: tests/neg/dedented-string-literals.scala:19:35
12+
| dedented string literal cannot mix tabs and spaces in indentation
13+
-- Error: ----------------------------------------------------------------------
1414
19 | val nonWhitespaceBeforeClosing = '''
1515
| ^
1616
|last line of dedented string literal must contain only whitespace before closing delimiter
17-
-- [E040] Syntax Error: tests/neg/dedented-string-literals.scala:41:17
17+
-- Error: ----------------------------------------------------------------------
1818
41 | val unclosed = '''
1919
| ^
20-
|unclosed dedented string literal
21-
-- Error: tests/neg/dedented-string-literals.scala:35:4
22-
35 | onlyAtCompileTime // error
23-
| ^^^^^^^^^^^^^^^^^
24-
|This method should only be used at compile time
25-
|Do not call at runtime
20+
| unclosed dedented string literal

tests/run/dedented-string-literals.check

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ Basic:
22
i am cow
33
hear me moo
44

5+
No Indent:
6+
7+
i am cow
8+
hear me moo
9+
510
With indent:
611
i am cow
712
hear me moo

0 commit comments

Comments
 (0)