Skip to content

Commit 300f300

Browse files
committed
.
1 parent 17205d9 commit 300f300

File tree

3 files changed

+76
-35
lines changed

3 files changed

+76
-35
lines changed

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

Lines changed: 56 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1353,33 +1353,73 @@ object Parsers {
13531353
* The amount of whitespace to remove is determined by the indentation
13541354
* of the last line (which should contain only whitespace before the
13551355
* closing delimiter).
1356+
*
1357+
* @param str The string content to dedent
1358+
* @param offset The source offset where the string literal begins
1359+
* @return The dedented string, or str if errors were reported
13561360
*/
1357-
private def dedentString(str: String): String = {
1361+
private def dedentString(str: String, offset: Offset): String = {
13581362
if (str.isEmpty) return str
13591363

13601364
// Find the last line (should be just whitespace before closing delimiter)
13611365
val lastNewlineIdx = str.lastIndexOf('\n')
1362-
assert(
1363-
lastNewlineIdx >= 0,
1364-
"Dedented string literal must contain at least two newlines"
1365-
)
1366+
if (lastNewlineIdx < 0) {
1367+
syntaxError(
1368+
em"dedented string literal must start with newline after opening quotes",
1369+
offset
1370+
)
1371+
return str
1372+
}
13661373

13671374
val closingIndent = str.substring(lastNewlineIdx + 1)
1368-
assert(
1369-
closingIndent.forall(_.isWhitespace),
1370-
"Last line of a dedented string literal must contain only whitespace followed by the closing delimiter"
1371-
)
1375+
if (!closingIndent.forall(_.isWhitespace)) {
1376+
syntaxError(
1377+
em"last line of dedented string literal must contain only whitespace before closing delimiter",
1378+
offset
1379+
)
1380+
return str
1381+
}
1382+
1383+
// Check for mixed tabs and spaces in closing indent
1384+
val hasTabs = closingIndent.contains('\t')
1385+
val hasSpaces = closingIndent.contains(' ')
1386+
if (hasTabs && hasSpaces) {
1387+
syntaxError(
1388+
em"dedented string literal cannot mix tabs and spaces in indentation",
1389+
offset
1390+
)
1391+
return str
1392+
}
1393+
13721394
// Split into lines
13731395
val lines = str.linesIterator.toSeq
13741396

13751397
// Process all lines except the last (which is just the closing indentation)
1398+
var lineOffset = offset
13761399
val dedented = lines.dropRight(1).map { line =>
1377-
if (line.startsWith(closingIndent)) line.substring(closingIndent.length)
1378-
else if (line.trim.isEmpty) "" // Empty or whitespace-only lines
1379-
else assert(
1380-
false,
1381-
s"line \"$line\" in dedented string must be either empty or be further indented than the closing delimiter"
1382-
)
1400+
val result =
1401+
if (line.startsWith(closingIndent)) line.substring(closingIndent.length)
1402+
else if (line.trim.isEmpty) "" // Empty or whitespace-only lines
1403+
else {
1404+
// Check if this line has mixed tabs/spaces that don't match closing indent
1405+
val lineIndent = line.takeWhile(_.isWhitespace)
1406+
val lineHasTabs = lineIndent.contains('\t')
1407+
val lineHasSpaces = lineIndent.contains(' ')
1408+
if ((hasTabs && lineHasSpaces && !lineHasTabs) || (hasSpaces && lineHasTabs && !lineHasSpaces)) {
1409+
syntaxError(
1410+
em"dedented string literal cannot mix tabs and spaces in indentation",
1411+
offset
1412+
)
1413+
} else {
1414+
syntaxError(
1415+
em"line in dedented string literal must be indented at least as much as the closing delimiter",
1416+
lineOffset
1417+
)
1418+
}
1419+
line
1420+
}
1421+
lineOffset += line.length + 1 // +1 for the newline
1422+
result
13831423
}
13841424

13851425
// Drop the first line if it's empty (the newline after opening delimiter)
@@ -1424,7 +1464,7 @@ object Parsers {
14241464
// For non-interpolated dedented strings, check if the token starts with '''
14251465
val str = in.strVal
14261466
if (token == STRINGLIT && !inStringInterpolation && isDedentedStringLiteral(negOffset)) {
1427-
dedentString(str)
1467+
dedentString(str, negOffset)
14281468
} else str
14291469
case TRUE => true
14301470
case FALSE => false

tests/neg/dedented-string-literals.check

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@
1010
13 | val mixedTabsSpaces = '''
1111
| ^
1212
|dedented string literal cannot mix tabs and spaces in indentation
13-
-- [E040] Syntax Error: tests/neg/dedented-string-literals.scala:19:17
14-
19 | val unclosed = '''
15-
| ^
16-
|unclosed dedented string literal
17-
-- [E040] Syntax Error: tests/neg/dedented-string-literals.scala:23:35
18-
23 | val nonWhitespaceBeforeClosing = '''
13+
-- Error: tests/neg/dedented-string-literals.scala:19:35
14+
19 | val nonWhitespaceBeforeClosing = '''
1915
| ^
20-
|unclosed dedented string literal
21-
-- Error: tests/neg/dedented-string-literals.scala:39:4
22-
39 | onlyAtCompileTime // error
16+
|last line of dedented string literal must contain only whitespace before closing delimiter
17+
-- Error: tests/neg/dedented-string-literals.scala:35:4
18+
35 | onlyAtCompileTime // error
2319
| ^^^^^^^^^^^^^^^^^
2420
|This method should only be used at compile time
2521
|Do not call at runtime
22+
-- [E040] Syntax Error: tests/neg/dedented-string-literals.scala:41:17
23+
41 | val unclosed = '''
24+
| ^
25+
|unclosed dedented string literal
Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,24 @@
11
// Test error cases for dedented string literals
22

3-
object DedentedStringErrors { // nopos-error // nopos-error // nopos-error
3+
object DedentedStringErrors {
44
// Error: No newline after opening quotes
5-
val noNewlineAfterOpen = '''content on same line // error
5+
val noNewlineAfterOpen = '''content on same line // error: dedented string literal must start with a newline
66

77
// Error: Content not indented enough
88
val notIndented = '''
9-
content
9+
content // error: line in dedented string literal is indented less than the closing delimiter
1010
'''
1111

12-
// Error: Mixed tabs and spaces
12+
// Error: Mixed tabs and spaces - first line has tab, but closing delimiter has spaces
1313
val mixedTabsSpaces = '''
14-
tab line
14+
tab line // error: line in dedented string literal is indented less than the closing delimiter
1515
space line
1616
'''
1717

18-
// Error: Unclosed literal
19-
val unclosed = '''
20-
some content
21-
2218
// Error: Non-whitespace before closing delimiter
2319
val nonWhitespaceBeforeClosing = '''
2420
content here
25-
text'''
21+
text''' // error: last line of dedented string literal must contain only whitespace before closing delimiter
2622
}
2723

2824
// Test @compileTimeOnly with dedented string
@@ -39,3 +35,8 @@ object CompileTimeOnlyTest {
3935
onlyAtCompileTime // error
4036
}
4137
}
38+
39+
// Error: Unclosed literal - must be last since it breaks parsing
40+
object UnclosedTest {
41+
val unclosed = ''' // error: unclosed dedented string literal
42+
some content

0 commit comments

Comments
 (0)